#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/pgpool/include/pgpool3.h>
#include <yandex/maps/wiki/revision/common.h>
#include <yandex/maps/wiki/revision/filters.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <yandex/maps/wiki/revision/branch_manager.h>
#include <yandex/maps/wiki/revision/commit_manager.h>
#include <yandex/maps/wiki/revisionapi/revisionapi.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>
#include <yandex/maps/wiki/common/extended_xml_doc.h>
#include <yandex/maps/wiki/common/default_config.h>
#include <yandex/maps/wiki/common/revert.h>

#include <boost/program_options/variables_map.hpp>
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/parsers.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/format.hpp>
#include <boost/filesystem.hpp>

#include <algorithm>
#include <fstream>
#include <iostream>
#include <iterator>
#include <sstream>
#include <unordered_map>
#include <vector>

namespace pgpool = maps::pgpool3;
namespace po = boost::program_options;
namespace rev = maps::wiki::revision;
namespace filters = rev::filters;
namespace revapi = maps::wiki::revisionapi;
namespace common = maps::wiki::common;
namespace fs = boost::filesystem;

namespace {

const std::string OPT_JSON_BATCH_SIZE = "json-batch-size";
const std::string OPT_ALL_COMMITS = "all-commits";
const std::string OPT_COMMIT_BATCH_SIZE = "commit-batch-size";
const std::string OPT_BRANCH = "branch";
const std::string OPT_CATEGORIES = "categories";
const std::string OPT_CFG = "cfg";
const std::string OPT_CMD = "cmd";
const std::string OPT_COMMIT_ID = "commit-id";
const std::string OPT_COMMIT_IDS = "commit-ids";
const std::string OPT_DRY_RUN = "dry-run";
const std::string OPT_HELP = "help";
const std::string OPT_OBJECT_IDS_FILE = "object-ids-file";
const std::string OPT_OUTPUT_FILE_PATTERN = "output-file-pattern";
const std::string OPT_OUTPUT_DIR = "output-dir";
const std::string OPT_SKIP_DANGLING_RELATIONS = "skip-dangling-relations";
const std::string OPT_INVERT_DIRECTION_FOR_ROLES = "invert-direction-for-roles";
const std::string OPT_VERBOSE = "verbose";
const std::string OPT_OUTPUT_FILE = "output-file";
const std::string OPT_PARAMS_CFG = "params-cfg";
const std::string OPT_PATH = "path";
const std::string OPT_IGNORE_IDS = "ignore-json-ids";
const std::string OPT_START_FROM_JSON = "start-from-json-id";
const std::string OPT_SEQUENCE_ID = "sequence-id";
const std::string OPT_USER_ID = "user-id";
const std::string OPT_BATCH_SIZE = "batch-size";
const std::string OPT_RELATIONS_EXPORT_MODE = "relations-export-mode";
const std::string OPT_BATCH_MODE = "batch-mode";
const std::string OPT_ID_OFFSET = "id-offset";
const std::string OPT_ID_COUNT = "id-count";
const std::string OPT_LOG_LEVEL = "log-level";

const std::string CFG_DATABASE_NAME = "long-read";
const std::string CFG_POOL_NAME = "revisionapi";
const std::string CFG_DEFAULT_OUTPUT_FILE_PATTERN = "%|08d|.json";
const size_t CFG_DEFAULT_COMMIT_BATCH_SIZE = 10000;

const std::string USAGE =
std::string("Usage: \n") +
"revisionapi " +
    "[--" + OPT_LOG_LEVEL + "=debug|info|warn|error|fatal] " +
    "--" + OPT_CMD + "=import " +
    "--" + OPT_USER_ID + "=<USER_ID> " +
    "[--" + OPT_PATH + "=<PATH> | --" + OPT_BATCH_MODE + "] " +
    "[--" + OPT_IGNORE_IDS + " | --" + OPT_START_FROM_JSON + "] " +
    "[--" + OPT_COMMIT_BATCH_SIZE + "=<BATCH_SIZE>] " +
    "[--" + OPT_ID_OFFSET + "=<OFFSET>] " +
    "[--" + OPT_CFG + "=<CONFIG>]\n" +
"revisionapi " +
    "[--" + OPT_LOG_LEVEL + "=debug|info|warn|error|fatal] " +
    "--" + OPT_CMD + "=export " +
    "--" + OPT_BRANCH + "=<BRANCH> " +
    "[--" + OPT_PARAMS_CFG + "=<PARAMS_CONFIG>] " +
    "[--" + OPT_RELATIONS_EXPORT_MODE + "=none|masterToSlave|slaveToMaster|both] " +
    "[--" + OPT_CATEGORIES + "=<CATEGORIES>|--" + OPT_OBJECT_IDS_FILE + "=<OIDS_FILE>] " +
    "[--" + OPT_SKIP_DANGLING_RELATIONS + "] " +
    "[--" + OPT_INVERT_DIRECTION_FOR_ROLES + "=<comma separated list>] " +
    "[--" + OPT_JSON_BATCH_SIZE + "=<max number of objects in one json file>] " +
    "[--" + OPT_BATCH_SIZE + "=<any number>] " +
    "[--" + OPT_OUTPUT_FILE_PATTERN + "=<format-string>] " +
    "[--" + OPT_OUTPUT_FILE + "=<OUTPUT_FILE>] " +
    "[--" + OPT_OUTPUT_DIR + "=<dir where to create file(s)>] " +
    "[--" + OPT_COMMIT_ID + "=<COMMIT_ID>] " +
    "[--" + OPT_CFG + "=<CONFIG>]\n" +
"revisionapi " +
    "[--" + OPT_LOG_LEVEL + "=debug|info|warn|error|fatal] " +
    "--" + OPT_CMD + "=delete " +
    "--" + OPT_USER_ID + "=<USER_ID> " +
    "--" + OPT_OBJECT_IDS_FILE + "=<OIDS_FILE> " +
    "[--" + OPT_COMMIT_BATCH_SIZE + "=<BATCH_SIZE>] " +
    "[--" + OPT_CFG + "=<CONFIG>]\n" +
"revisionapi " +
    "[--" + OPT_LOG_LEVEL + "=debug|info|warn|error|fatal] " +
    "--" + OPT_CMD + "=approve " +
    "(--" + OPT_ALL_COMMITS + " | " +
     "--" + OPT_COMMIT_IDS + "=<COMMIT_IDS>) " +
    "[--" + OPT_DRY_RUN + "] " +
    "[--" + OPT_CFG + "=<CONFIG>]\n" +
"revisionapi " +
    "[--" + OPT_LOG_LEVEL + "=debug|info|warn|error|fatal] " +
    "--" + OPT_CMD + "=headCommitId " +
    "--" + OPT_BRANCH + "=<BRANCH> " +
    "[--" + OPT_CFG + "=<CONFIG>]\n" +
"revisionapi " +
    "[--" + OPT_LOG_LEVEL + "=debug|info|warn|error|fatal] " +
    "--" + OPT_CMD + "=createApproved " +
    "--" + OPT_USER_ID + "=<USER_ID> " +
    "[--" + OPT_DRY_RUN + "] " +
    "[--" + OPT_CFG + "=<CONFIG>]\n" +
"revisionapi " +
    "[--" + OPT_LOG_LEVEL + "=debug|info|warn|error|fatal] " +
    "--" + OPT_CMD + "=createStable " +
    "--" + OPT_USER_ID + "=<USER_ID> " +
    "[--" + OPT_DRY_RUN + "] " +
    "[--" + OPT_CFG + "=<CONFIG>]\n" +
"revisionapi " +
    "[--" + OPT_LOG_LEVEL + "=debug|info|warn|error|fatal] " +
    "--" + OPT_CMD + "=closeStable " +
    "--" + OPT_USER_ID + "=<USER_ID> " +
    "[--" + OPT_DRY_RUN + "] " +
    "[--" + OPT_CFG + "=<CONFIG>]\n" +
"revisionapi " +
    "[--" + OPT_LOG_LEVEL + "=debug|info|warn|error|fatal] " +
    "--" + OPT_CMD + "=approvedToStable " +
    "[--" + OPT_DRY_RUN + "] " +
    "[--" + OPT_CFG + "=<CONFIG>]\n" +
"revisionapi " +
    "[--" + OPT_LOG_LEVEL + "=debug|info|warn|error|fatal] " +
    "--" + OPT_CMD + "=clearAll " +
    "[--" + OPT_SEQUENCE_ID + "=<SEQID>] " +
    "[--" + OPT_CFG + "=<CONFIG>]\n" +
"revisionapi " +
    "[--" + OPT_LOG_LEVEL + "=debug|info|warn|error|fatal] " +
    "--" + OPT_CMD + "=reserveObjectIds " +
    "--" + OPT_ID_COUNT + "=<NUMBER> " +
    "[--" + OPT_CFG + "=<CONFIG>]\n" +
"revisionapi " +
    "[--" + OPT_LOG_LEVEL + "=debug|info|warn|error|fatal] " +
    "--" + OPT_CMD + "=revert " +
    "--" + OPT_USER_ID + "=<USER_ID> " +
     "--" + OPT_COMMIT_IDS + "=<COMMIT_IDS> " +
    "[--" + OPT_DRY_RUN + "] " +
    "[--" + OPT_CFG + "=<CONFIG>]\n";

} // namespace

namespace maps {
namespace wiki {
namespace revisionapi {
namespace tool {

enum Command {
    Import,
    Approve,
    Revert,
    Export,
    Delete,
    HeadCommitId,
    CreateApproved,
    CreateStable,
    CloseStable,
    ApprovedToStable,
    ClearAll,
    ReserveObjectIds
};

std::istream& operator>>(std::istream& is, Command& cmd)
{
    const std::unordered_map<std::string, Command> cmap {
        { "import",             Import },
        { "export",             Export },
        { "delete",             Delete },
        { "approve",            Approve },
        { "revert",             Revert},
        { "headCommitId",       HeadCommitId },
        { "createApproved",     CreateApproved },
        { "createStable",       CreateStable },
        { "closeStable",        CloseStable },
        { "approvedToStable",   ApprovedToStable },
        { "clearAll",           ClearAll },
        { "reserveObjectIds",   ReserveObjectIds } };

    std::string cmdStr;
    is >> cmdStr;
    auto it = cmap.find(cmdStr);
    if (it == cmap.end()) {
        throw maps::RuntimeError()
            << "couldn't parse command from string '" << cmdStr << "'";
    }
    cmd = it->second;
    return is;
}

RelationsExportFlags modeStringToRelationsExportFlags(std::string s)
{
    const std::unordered_map<std::string, RelationsExportFlags> map {
        { "none", RelationsExportFlags::None },
        { "mastertoslave", RelationsExportFlags::MasterToSlave },
        { "slavetomaster", RelationsExportFlags::SlaveToMaster },
        { "both", RelationsExportFlags::MasterToSlave
            | RelationsExportFlags::SlaveToMaster }
    };
    boost::to_lower(s, std::locale("C"));
    auto it = map.find(s);
    if (it == map.end()) {
        throw maps::RuntimeError()
            << "'" << s << "' is not a valid value for '" << OPT_RELATIONS_EXPORT_MODE << "' option";
    }
    return it->second;
}

template <typename Container>
Container ensureParamValues(
    const po::variables_map& vm,
    const std::string& param,
    const char delimiter = ',')
{
    REQUIRE(vm.count(param), "specify parameter '" << param << "'");
    auto str = vm[param].as<std::string>();

    Container result;
    std::list<std::string> parts;
    boost::split(parts, str, [&](char c) { return c == delimiter; });

    std::insert_iterator<Container> it(result, result.end());
    for (const auto& part : parts) {
        if (!part.empty()) {
            *it = boost::lexical_cast<typename Container::value_type>(part);
        }
    }
    return result;
}

template <typename ParamType>
ParamType ensureParamValue(
    const po::variables_map& vm,
    const std::string& param)
{
    REQUIRE(vm.count(param), "specify parameter '" << param << "'");
    return vm[param].as<ParamType>();
}

void ensureDirectory(const fs::path& path) {
    if (fs::exists(path)) {
        REQUIRE(fs::is_directory(path), "output dir path '" + path.string()
            + "' exists and is not a directory");
    } else {
        fs::create_directories(path);
    }
}

template <typename Container>
std::string stringifyCommitIds(const Container& commitIds)
{
    std::string result;
    for (const auto& commitId : commitIds) {
        result += (result.empty() ? "" : ",") + std::to_string(commitId);
    }
    return result;
}

std::list<rev::DBID> doImport(pgpool::Pool& pool, const rev::UserID& userId,
    IdMode idMode,
    std::istream& stream,
    size_t commitBatchSize,
    size_t idOffset)
{
    RevisionAPI revisionAPI(pool);
    return revisionAPI.importData(userId, idMode, stream, commitBatchSize, idOffset);
}

std::list<rev::DBID> doImportBatch(
    std::istream& filePathsStream,
    pgpool::Pool& pool,
    const rev::UserID& userID,
    IdMode idMode,
    size_t commitBatchSize,
    size_t idOffset)
{
    std::list<rev::DBID> commitIds;
    for (std::string path; std::getline(filePathsStream, path).good() && !path.empty();) {
        std::ifstream stream(path);
        REQUIRE(!stream.fail(), "Cannot open '" << path << "' for reading");
        commitIds.splice(commitIds.end(), doImport(pool, userID,
                idMode, stream, commitBatchSize, idOffset));
        std::cout << "completed: " << path << std::endl;
    }
    return commitIds;
}

std::string processImport(
    pgpool::Pool& pool, const po::variables_map& vm)
{
    rev::UserID userId = ensureParamValue<rev::UserID>(vm, OPT_USER_ID);
    size_t commitBatchSize = CFG_DEFAULT_COMMIT_BATCH_SIZE;
    if (vm.count(OPT_COMMIT_BATCH_SIZE)) {
        commitBatchSize = ensureParamValue<size_t>(vm, OPT_COMMIT_BATCH_SIZE);
    }

    std::ifstream fs;
    if (vm.count(OPT_PATH)) {
        REQUIRE(
                !vm.count(OPT_BATCH_MODE),
                "--path <path> and --batch-mode are mutually exclusive, "
                "please use only one");
        std::string path(ensureParamValue<std::string>(vm, OPT_PATH));
        fs.open(path);
        REQUIRE(!fs.fail(), "couldn't open '" << path << "' for reading");
    }

    IdMode idMode = IdMode::PreserveId;
    size_t idOffset = 0;

    if (vm.count(OPT_IGNORE_IDS))
        idMode = IdMode::IgnoreJsonId;
    if (vm.count(OPT_START_FROM_JSON))
        idMode = IdMode::StartFromJsonId;
    if (vm.count(OPT_IGNORE_IDS) + vm.count(OPT_START_FROM_JSON) > 1)
        throw maps::RuntimeError()
            << "unable to use more then one id-mode parameter at once";
    if (vm.count(OPT_ID_OFFSET)) {
        REQUIRE(idMode == IdMode::PreserveId,
            "--id-offset is only valid without --ignore-ids and --start-from-json");
        idOffset = ensureParamValue<size_t>(vm, OPT_ID_OFFSET);
    }

    if (vm.count(OPT_BATCH_MODE)) {
        return "commit-ids: " + stringifyCommitIds(
            doImportBatch(
                std::cin, pool, userId, idMode,
                commitBatchSize, idOffset));
    }

    std::istream& stream =
        fs.is_open()
        ? fs
        : std::cin;

    return stringifyCommitIds(doImport(pool, userId, idMode,
            stream, commitBatchSize, idOffset));
}

std::string processDryRunOption(
    pqxx::transaction_base& txn, const po::variables_map& vm)
{
    if (vm.count(OPT_DRY_RUN)) {
        return "commit into database is skipped\n";
    }
    txn.commit();
    return {};
}

std::string processApprove(
    pgpool::Pool& pool, const po::variables_map& vm)
{
    auto writeTr = pgpool::makeWriteableTransaction(pool.getMasterConnection());
    rev::CommitManager commitMgr(*writeTr);

    rev::DBIDSet commitIds;
    if (vm.count(OPT_ALL_COMMITS)) {
        auto maxCommitId = vm.count(OPT_COMMIT_ID)
            ? ensureParamValue<rev::DBID>(vm, OPT_COMMIT_ID)
            : rev::RevisionsGateway(*writeTr).headCommitId();
        commitIds = commitMgr.approveAll(maxCommitId);
    } else {
        commitIds = commitMgr.approve(
            ensureParamValues<rev::DBIDSet>(vm, OPT_COMMIT_IDS));
    }

    std::stringstream result;
    result << processDryRunOption(*writeTr, vm);

    result << "approve successful, approved commit count: " << commitIds.size() << std::endl;
    result << "commit-ids: " << stringifyCommitIds(commitIds);
    return result.str();
}

std::unique_ptr<ExportParams>
getExportParams(const po::variables_map& vm, const rev::Branch& branch, rev::DBID commitId)
{
    if (vm.count(OPT_PARAMS_CFG)) {
        return ExportParams::loadFromFile(
            branch, commitId, vm[OPT_PARAMS_CFG].as<std::string>());
    }
    auto flags = modeStringToRelationsExportFlags(
        vm[OPT_RELATIONS_EXPORT_MODE].as<std::string>());
    if (vm.count(OPT_SKIP_DANGLING_RELATIONS)) {
        flags = flags | RelationsExportFlags::SkipDangling;
    }

    std::vector<std::string> invertDirectionRoles;
    if (vm.count(OPT_INVERT_DIRECTION_FOR_ROLES)) {
        invertDirectionRoles =
            ensureParamValues<std::vector<std::string>>(
                vm, OPT_INVERT_DIRECTION_FOR_ROLES);
    }
    return std::unique_ptr<ExportParams>(new ExportParams(
        branch, commitId, flags, invertDirectionRoles));
}

std::string processExport(
    pgpool::Pool& pool, const po::variables_map& vm)
{
    std::string outputFilePattern = CFG_DEFAULT_OUTPUT_FILE_PATTERN;
    fs::path outputDir = fs::initial_path();
    size_t writeBatchSize = 0;

    if (vm.count(OPT_JSON_BATCH_SIZE)) {
        writeBatchSize = ensureParamValue<size_t>(vm, OPT_JSON_BATCH_SIZE);
        REQUIRE(writeBatchSize, "Option '" + OPT_JSON_BATCH_SIZE
            + "' requires its value to be >0.");
        REQUIRE(!vm.count(OPT_OUTPUT_FILE), "Option '" + OPT_OUTPUT_FILE
            + "' conflicts with '" + OPT_JSON_BATCH_SIZE
            + "'. With '" + OPT_JSON_BATCH_SIZE + "' please use '"
            + OPT_OUTPUT_FILE_PATTERN + "' and '" + OPT_OUTPUT_DIR + "'.");
        if (vm.count(OPT_OUTPUT_FILE_PATTERN)) {
            outputFilePattern = ensureParamValue<std::string>(vm, OPT_OUTPUT_FILE_PATTERN);
        }
    }

    if (vm.count(OPT_OUTPUT_DIR)) {
        outputDir = fs::complete(ensureParamValue<std::string>(vm, OPT_OUTPUT_DIR));
    }
    ensureDirectory(outputDir);

    std::ofstream fs;
    if (vm.count(OPT_OUTPUT_FILE)) {
        fs::path pathFromCl(ensureParamValue<std::string>(vm, OPT_OUTPUT_FILE));
        fs::path path = pathFromCl.is_complete()
            ? pathFromCl
            : outputDir / pathFromCl;
        fs.open(path.string());
        REQUIRE(!fs.fail(), "couldn't open file: " << path.string());
    }

    auto getBranchAndCommitId = [&]() {
        auto readTr = pgpool::makeReadOnlyTransaction(pool.getSlaveConnection());
        auto branch = rev::BranchManager(*readTr).loadByString(
            ensureParamValue<std::string>(vm, OPT_BRANCH));

        auto commitId = vm.count(OPT_COMMIT_ID)
            ? ensureParamValue<rev::DBID>(vm, OPT_COMMIT_ID)
            : rev::RevisionsGateway(*readTr, branch).headCommitId();
        return std::make_pair(branch, commitId);
    };
    const auto result = getBranchAndCommitId();
    const auto& branch = result.first;
    const auto commitId = result.second;
    REQUIRE(commitId, "export from empty database");

    FilterPtr filter;
    if (vm.count(OPT_CATEGORIES)) {
        REQUIRE(!vm.count(OPT_OBJECT_IDS_FILE),
            "--" << OPT_CATEGORIES << " and --" << OPT_OBJECT_IDS_FILE
            << " are mutually exclusive, please use only one");

        std::vector<std::string> categories;
        std::string param = ensureParamValue<std::string>(vm, OPT_CATEGORIES);
        boost::split(categories, param, boost::is_any_of(","));
        for (auto& category : categories) {
            category = "cat:" + category;
        }

        filter.reset(new filters::ProxyFilterExpr(
            filters::Attr::definedAny(categories)));
    }

    std::vector<rev::DBID> objectIds;
    if (vm.count(OPT_OBJECT_IDS_FILE)) {
        auto fname = ensureParamValue<std::string>(vm, OPT_OBJECT_IDS_FILE);
        std::ifstream oidsStream(fname);
        REQUIRE(oidsStream, "couldn't open file: " << fname);

        std::copy(
            std::istream_iterator<rev::DBID>(oidsStream),
            std::istream_iterator<rev::DBID>(),
            std::back_inserter(objectIds));
    }

    GetStreamForChunkFunc callback =
        SingleStreamWrapper(
            vm.count(OPT_OUTPUT_FILE) ? fs : std::cout);

    if (writeBatchSize) {
        if (vm.count(OPT_VERBOSE)) {
            std::cerr << "writing chunked json, file name pattern='" << outputFilePattern << "'" << std::endl;
        }
        callback = [&](size_t chunkNo)
        {
            auto path  = outputDir / (boost::format(outputFilePattern) % chunkNo).str();
            if (vm.count(OPT_VERBOSE)) {
                std::cerr << "writing '" << path.string() << "'" << std::endl;
            }
            std::unique_ptr<std::ostream> stream(new std::ofstream(path.string()));
            REQUIRE(!stream->fail(), "error opening '" + path.string() + "' for writing");
            return stream;
        };
    }

    auto params = getExportParams(vm, branch, commitId);
    params->setWriteBatchSize(writeBatchSize);
    if (vm.count(OPT_OBJECT_IDS_FILE)) {
        RevisionAPI(pool).exportData(*params, callback, objectIds);
    } else {
        RevisionAPI(pool).exportData(*params, callback, filter);
    }

    return "";
}

std::list<rev::DBID> doDelete(
    pgpool::Pool& pool,
    const rev::UserID& userId,
    const std::vector<rev::DBID>& objectIds,
    size_t commitBatchSize)
{
    RevisionAPI revisionAPI(pool);
    return revisionAPI.deleteData(userId, objectIds, commitBatchSize);
}

std::string processDelete(
    pgpool::Pool& pool, const po::variables_map& vm)
{
    rev::UserID userId = ensureParamValue<rev::UserID>(vm, OPT_USER_ID);
    size_t commitBatchSize = CFG_DEFAULT_COMMIT_BATCH_SIZE;
    if (vm.count(OPT_COMMIT_BATCH_SIZE)) {
        commitBatchSize = ensureParamValue<size_t>(vm, OPT_COMMIT_BATCH_SIZE);
    }

    std::vector<rev::DBID> objectIds;
    if (vm.count(OPT_OBJECT_IDS_FILE)) {
        auto fname = ensureParamValue<std::string>(vm, OPT_OBJECT_IDS_FILE);
        std::ifstream oidsStream(fname);
        REQUIRE(oidsStream, "couldn't open file: " << fname);

        std::copy(
            std::istream_iterator<rev::DBID>(oidsStream),
            std::istream_iterator<rev::DBID>(),
            std::back_inserter(objectIds));
    }

    return stringifyCommitIds(doDelete(pool, userId, objectIds, commitBatchSize));
}

std::string processHeadCommitId(
    pgpool::Pool& pool, const po::variables_map& vm)
{
    auto readTr = pgpool::makeReadOnlyTransaction(pool.getSlaveConnection());
    auto branch = rev::BranchManager(*readTr).loadByString(
        ensureParamValue<std::string>(vm, OPT_BRANCH));
    rev::RevisionsGateway gateway(*readTr, branch);

    return std::to_string(gateway.headCommitId());
}

std::string processCreateApproved(
    pgpool::Pool& pool, const po::variables_map& vm)
{
    rev::UserID userId = ensureParamValue<rev::UserID>(vm, OPT_USER_ID);

    auto writeTr = pgpool::makeWriteableTransaction(pool.getMasterConnection());

    auto branchId = rev::BranchManager(*writeTr).createApproved(userId, {}).id();

    std::stringstream result;
    result << processDryRunOption(*writeTr, vm);

    result << "Approved branch created, id: " << branchId;
    return result.str();
}

std::string processCreateStable(
    pgpool::Pool& pool, const po::variables_map& vm)
{
    rev::UserID userId = ensureParamValue<rev::UserID>(vm, OPT_USER_ID);

    auto writeTr = pgpool::makeWriteableTransaction(pool.getMasterConnection());

    auto stableBranchId = rev::BranchManager(*writeTr).createStable(userId, {}).id();

    std::stringstream result;
    result << processDryRunOption(*writeTr, vm);

    result << "Stable branch created, id: " << stableBranchId;
    return result.str();
}

std::string processCloseStable(
    pgpool::Pool& pool, const po::variables_map& vm)
{
    rev::UserID userId = ensureParamValue<rev::UserID>(vm, OPT_USER_ID);

    auto writeTr = pgpool::makeWriteableTransaction(pool.getMasterConnection());
    auto branch = rev::BranchManager(*writeTr).loadStable();
    bool success = branch.finish(*writeTr, userId);

    std::stringstream result;
    result << processDryRunOption(*writeTr, vm);

    result << (success ? "CLOSED" : "SKIPPED");
    return result.str();
}

std::string processApprovedToStable(
    pgpool::Pool& pool, const po::variables_map& vm)
{
    auto writeTr = pgpool::makeWriteableTransaction(pool.getMasterConnection());
    rev::RevisionsGateway gateway(*writeTr);
    auto headCommitId = gateway.headCommitId();
    REQUIRE(headCommitId, "empty database");

    auto commitIds = rev::CommitManager(*writeTr).mergeApprovedToStable(headCommitId);

    std::stringstream result;
    result << processDryRunOption(*writeTr, vm);

    result << "approved -> stable, commit count: " << commitIds.size() << std::endl;
    result << "commit-ids: " << stringifyCommitIds(commitIds);
    return result.str();
}

std::string processClearAll(
    pgpool::Pool& pool,
    const po::variables_map& vm)
{
    auto writeTr = pgpool::makeWriteableTransaction(pool.getMasterConnection());
    rev::RevisionsGateway gateway(*writeTr);
    gateway.truncateAll();
    gateway.createDefaultBranches();
    if (vm.count(OPT_SEQUENCE_ID)) {
        auto seqId = ensureParamValue<rev::DBID>(vm, OPT_SEQUENCE_ID);
        REQUIRE(seqId, "<SEQID> should be greater then 0");
        gateway.acquireObjectIds(seqId);
    }
    writeTr->commit();
    return "OK";
}

std::string processReserveObjectIds(
    pgpool::Pool& pool,
    const po::variables_map& vm)
{
    auto writeTr = pgpool::makeWriteableTransaction(pool.getMasterConnection());
    rev::RevisionsGateway gateway(*writeTr);
    size_t count = ensureParamValue<size_t>(vm, OPT_ID_COUNT);
    rev::DBID max = gateway.reserveObjectIds(count);
    return std::to_string(max);
}

std::string processRevertCommitsInTrunk(pgpool::Pool& pool, const po::variables_map& vm)
{
    auto writeTr = pgpool::makeWriteableTransaction(pool.getMasterConnection());
    rev::CommitManager commitMgr(*writeTr);

    const auto userId = ensureParamValue<rev::UserID>(vm, OPT_USER_ID);
    const auto commitIds = ensureParamValues<rev::DBIDSet>(vm, OPT_COMMIT_IDS);
    const auto data = commitMgr.revertCommitsInTrunk(commitIds, userId,
        {
            {common::REVERT_COMMIT_ACTION_KEY, common::REVERT_COMMIT_ACTION_VALUE},
            {common::REVERT_COMMIT_IDS_KEY, stringifyCommitIds(commitIds)}
        }
    );

    std::stringstream out;

    out << "Create commit id " << data.createdCommit.id() << std::endl;
    out << "Reverted commits (" << data.revertedCommitIds.size() << "):" << std::endl;
    out << stringifyCommitIds(data.revertedCommitIds) << std::endl;
    out << processDryRunOption(*writeTr, vm);

    return out.str();
}

void switchCommand(const Command cmd, const po::variables_map& vm, pgpool::Pool& pool)
{
    switch (cmd) {
        case Import:
            std::cout << processImport(pool, vm) << std::endl; break;
        case Approve:
            std::cout << processApprove(pool, vm) << std::endl; break;
        case Export:
            std::cout << processExport(pool, vm) << std::endl; break;
        case HeadCommitId:
            std::cout << processHeadCommitId(pool, vm) << std::endl; break;
        case CreateApproved:
            std::cout << processCreateApproved(pool, vm) << std::endl; break;
        case CreateStable:
            std::cout << processCreateStable(pool, vm) << std::endl; break;
        case CloseStable:
            std::cout << processCloseStable(pool, vm) << std::endl; break;
        case ApprovedToStable:
            std::cout << processApprovedToStable(pool, vm) << std::endl; break;
        case ClearAll:
            std::cout << processClearAll(pool, vm) << std::endl; break;
        case ReserveObjectIds:
            std::cout << processReserveObjectIds(pool, vm) << std::endl; break;
        case Revert:
            std::cout << processRevertCommitsInTrunk(pool, vm) << std::endl; break;
        case Delete:
            std::cout << processDelete(pool, vm) << std::endl; break;

    }
}

std::unique_ptr<common::ExtendedXmlDoc> loadConfig(const po::variables_map& vm)
{
    if (vm.count(OPT_CFG)) {
        return std::unique_ptr<common::ExtendedXmlDoc>(
            new common::ExtendedXmlDoc(ensureParamValue<std::string>(vm, OPT_CFG)));
    } else {
        return common::loadDefaultConfig();
    }
}

void withPoolSwitchCommand(const Command cmd, const po::variables_map& vm)
{
    std::unique_ptr<common::ExtendedXmlDoc> doc(loadConfig(vm));
    if (vm.count(OPT_VERBOSE)) {
        bool first = true;
        for (auto &p: doc->sourcePaths()) {
            if (first) {
                std::cerr << "config path: ";
                first = false;
            } else {
                std::cerr << "   based on: ";
            }
            std::cerr << p << std::endl;
        }
    }

    auto overrideConstants = [](pgpool::PoolConstants p)
    {
        p.timeoutEarlyOnMasterUnavailable = false;
        return p;
    };

    common::PoolHolder poolHolder(overrideConstants, *doc, CFG_DATABASE_NAME, CFG_POOL_NAME);
    RevisionAPI revisionAPI(poolHolder.pool());

    switchCommand(cmd, vm, poolHolder.pool());
}

void processLogLevel(const po::variables_map& vm)
{
    if (vm.count(OPT_LOG_LEVEL)) {
        log8::setLevel(vm[OPT_LOG_LEVEL].as<std::string>());
    } else {
        log8::setLevel(log8::Level::FATAL);
    }
}

} // namespace tool
} // namespace revisionapi
} // namespace wiki
} // namespace maps

int main(int argc, char* argv[])
{
    po::options_description desc(USAGE + "description:");
    desc.add_options()
        (OPT_LOG_LEVEL.c_str(), po::value<std::string>(), "set maps::Log log level")
        (OPT_DRY_RUN.c_str(), "simulate operation (without commit)")
        (OPT_ALL_COMMITS.c_str(), "apply command to all commits")
        (OPT_COMMIT_BATCH_SIZE.c_str(), po::value<size_t>(),
            "number of objects written to db in single transaction"
            "during import")
        (OPT_JSON_BATCH_SIZE.c_str(), po::value<size_t>(),
            "max number of objects in one json file,"
            "turns on json splitting mode")
        (OPT_BATCH_SIZE.c_str(), po::value<size_t>(),
            "no-op, for backward compatibility only")
        (OPT_BRANCH.c_str(), po::value<std::string>()->default_value(
            "trunk"), "branch id (trunk, approved, stable, <id>)")
        (OPT_CATEGORIES.c_str(), po::value<std::string>(),
            "categories list sepatated by commas")
        (OPT_OBJECT_IDS_FILE.c_str(), po::value<std::string>(),
            "read object ids to export from file")
        (OPT_SKIP_DANGLING_RELATIONS.c_str(),
            "skip relations with non-exported objects")
        (OPT_INVERT_DIRECTION_FOR_ROLES.c_str(), po::value<std::string>(),
            "invert direction for specific relation roles")
        (OPT_CFG.c_str(), po::value<std::string>(), "config")
        (OPT_PARAMS_CFG.c_str(), po::value<std::string>(), "printed config for export (json-file)")
        (OPT_CMD.c_str(), po::value<std::string>(), "command (import, export, approve...)")
        (OPT_COMMIT_ID.c_str(), po::value<rev::DBID>(), "commit id")
        (OPT_COMMIT_IDS.c_str(), po::value<std::string>(), "comma separated commit ids")
        (OPT_HELP.c_str(), "produce help message")
        (OPT_OUTPUT_FILE.c_str(), po::value<std::string>(),
            "redirect output stream to the file")
        (OPT_OUTPUT_FILE_PATTERN.c_str(), po::value<std::string>(),
            "pattern for output file names, is a boost format-string,"
            "fed with a single argument - zero-based number of chunk")
        (OPT_OUTPUT_DIR.c_str(), po::value<std::string>(),
            "output dir, will be created if not exists,"
            "defaults to current dir")
        (OPT_PATH.c_str(), po::value<std::string>(), "path to json data file (default stdin)")
        (OPT_VERBOSE.c_str(), "print to stderr various unnecessary details on what's happening")
        (OPT_IGNORE_IDS.c_str(),  "ignore ids from json, generate new ids for result data")
        (OPT_START_FROM_JSON.c_str(), "start relation ids from max object id in input json")
        (OPT_ID_OFFSET.c_str(), po::value<size_t>(), "add offset to every json id, zero if this option is not given,"
            " only valid in 'preserve json ids' mode, which is default")
        (OPT_SEQUENCE_ID.c_str(), po::value<rev::DBID>(), "init sequence with specified SEQID after clearing all data")
        (OPT_USER_ID.c_str(), po::value<rev::UserID>(), "user id")
        (OPT_RELATIONS_EXPORT_MODE.c_str(), po::value<std::string>()->default_value(
            "mastertoslave") , "which kinds of relations to embed into object")
        (OPT_BATCH_MODE.c_str(), "import from multiple json files given on stdin, one path per line, "
            "program will print 'OK' to stdout after each file successfully processed")
        (OPT_ID_COUNT.c_str(), po::value<size_t>(), "difference between new sequence value and old last object id");

    po::variables_map vm;
    try {
        po::store(po::parse_command_line(argc, argv, desc), vm);
    } catch (po::error& e) {
        std::cerr << "error parsing command line: " << e.what() << std::endl;
        return 1;
    }
    po::notify(vm);

    // TODO: cleanup try-catch mess and duplicated error messages
    if (vm.count(OPT_CMD)) {
        try {
            revapi::tool::processLogLevel(vm);
            revapi::tool::withPoolSwitchCommand(
                boost::lexical_cast<revapi::tool::Command>(vm[OPT_CMD].as<std::string>()), vm);
        } catch (const std::exception& ex) {
            std::cerr << "FAIL: " << ex.what() << std::endl;

            try {
                throw;
            } catch (const maps::Exception &mex) {
                std::cerr << mex << std::endl;
            } catch(...) {
            }

            return 1;
        }
    } else {
        std::cout << desc << std::endl;
    }
    return 0;
}
