#include "../update_revision_attrs.h"

#include <yandex/maps/wiki/common/robot.h>
#include <yandex/maps/wiki/revision/common.h>

#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>

#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/program_options.hpp>

#include <string>
#include <fstream>
#include <iostream>

namespace po = boost::program_options;
using namespace maps::wiki;

namespace {

const revision::Attributes COMMIT_ATTRS =
     {{"action", "group-modified-attributes"}};

const std::string CATEGORY = "rd_el";
const std::string ATTR = "speed_limit";
const std::string TDS_ATTR = CATEGORY + ":" + ATTR;
const std::string TDS_CATEGORY = "cat:" + CATEGORY;

const std::string OPT_RESULT_COMMITS_FILE = "result-commits-file";
const std::string OPT_RESULT_DRAFT_OBJECTS_FILE = "result-draft-objects-file";
const std::string OPT_RESULT_NON_EXISTENT_OBJECTS_FILE = "result-nonexistent-objects-file";
const std::string OPT_FILE = "file";
const std::string OPT_HELP = "help";
const std::string OPT_CONN = "conn";
const std::string OPT_USER_ID = "user-id";
const std::string OPT_FORCE = "force";
const std::string OPT_BRANCH = "branch";
const std::string OPT_BATCH_SIZE = "batch-size";
const std::string OPT_BEFORE_ID = "before-id";
const std::string OPT_OVERWRITE = "overwrite";
const std::string OPT_APPROVED_ONLY = "approved-only";
const std::string OPT_DRY_RUN = "dry-run";

std::vector<std::string>
splitString(const std::string& str)
{
    std::vector<std::string> parts;
    auto delimiter = [] (char c)
    {
        return c == ',' || c == ';' || ::isspace(c);
    };
    boost::split(parts, str, delimiter);
    return parts;
}

template <typename Type>
Type
getValue(const po::variables_map& vm, const std::string& option)
{
    if (!vm.count(option)) {
        return {};
    }

    try {
        return vm[option].as<Type>();
    } catch (...) {
        ERROR() << "invalid value of option: " << option;
        throw;
    }
}

struct Params : public Options
{
    explicit Params(const po::variables_map& vm)
        : Options({
            vm.count(OPT_OVERWRITE) > 0,
            vm.count(OPT_APPROVED_ONLY) > 0,
            vm.count(OPT_DRY_RUN) > 0,
            getValue<revision::DBID>(vm, OPT_BRANCH),
            getValue<size_t>(vm, OPT_BATCH_SIZE)
            })
        , force(vm.count(OPT_FORCE) > 0)
        , beforeId(getValue<revision::DBID>(vm, OPT_BEFORE_ID))
        , inputFileName(getValue<std::string>(vm, OPT_FILE))
        , connString(getValue<std::string>(vm, OPT_CONN))
        , uid(getValue<revision::UserID>(vm, OPT_USER_ID))
        , resultCommitsFileName(getValue<std::string>(vm, OPT_RESULT_COMMITS_FILE))
        , resultDraftObjectsFileName(getValue<std::string>(vm, OPT_RESULT_DRAFT_OBJECTS_FILE))
        , resultNonExistentObjectsFileName(getValue<std::string>(vm, OPT_RESULT_NON_EXISTENT_OBJECTS_FILE))
    {
        REQUIRE(!inputFileName.empty(), OPT_FILE << " empty");
        REQUIRE(!connString.empty(), OPT_CONN << " empty");
        REQUIRE(uid, OPT_USER_ID << " zero");
    }

    const bool force;
    const revision::DBID beforeId;
    const std::string inputFileName;
    const std::string connString;
    const revision::UserID uid;
    const std::string resultCommitsFileName;
    const std::string resultDraftObjectsFileName;
    const std::string resultNonExistentObjectsFileName;
};

template <typename Container>
void
dumpContainer(const std::string& filename, const Container& container)
{
    if (filename.empty()) {
        return;
    }

    std::ofstream outfile(filename);
    for (const auto& value : container) {
        outfile << value << std::endl;
    }
}

void run(const po::variables_map& vm)
{
    const Params params(vm);

    AttributesData attrs;

    std::ifstream infile(params.inputFileName);
    REQUIRE(infile, "input file not exists: " << params.inputFileName);
    std::string str;
    size_t errors = 0;
    for (size_t line = 1; std::getline(infile, str); ++line) {
        if (str.empty()) {
            continue;
        }
        auto parts = splitString(str);
        for (auto& s : parts) {
            boost::trim(s);
        }

        auto reportError = [&](const std::string& msg)
        {
            ERROR() << "line: " << line << " " << msg << " : " << str;
            return false;
        };

        auto checkLine = [&]() -> bool
        {
            if (parts.size() < 2) {
                return reportError("must be two values: id speed_limit [comments]");
            }
            revision::DBID id = 0;
            size_t value = 0;
            try {
                id = boost::lexical_cast<revision::DBID>(parts[0]);
                value = boost::lexical_cast<size_t>(parts[1]);
            } catch (const std::exception& ) {
                return reportError("invalid numeric data");
            }
            if (id <= 0) {
                return reportError("invalid id");
            }
            if (value <= 0 || (value % 5) || value > 150) {
                return reportError("invalid value");
            }
            if (params.beforeId && id >= params.beforeId) {
                return true; // skip data
            }
            if (!attrs.insert({id, Attribute{TDS_ATTR, std::to_string(value)}}).second) {
                return reportError("object " + std::to_string(id) + " is already added");
            }
            return true;
        };
        errors += checkLine() ? 0 : 1;
    }

    INFO() << attrs.size() << " objects to process (" << errors << " errors)";

    if (!params.force && errors > 0) {
        return;
    }
    auto result = updateRevisionAttributes(
        params.connString, TDS_CATEGORY, attrs, COMMIT_ATTRS, params.uid, params);

    dumpContainer(params.resultCommitsFileName, result.commitIds);
    dumpContainer(params.resultDraftObjectsFileName, result.draftObjectIds);
    dumpContainer(params.resultNonExistentObjectsFileName, result.nonExistentObjectIds);
}

} //namespace

int main(int argc, char** argv)
{
    po::options_description desc("Usage: <tool> <options>\nOptions");
    desc.add_options()
        (OPT_HELP.c_str(),
            "show help message")
        (OPT_CONN.c_str(), po::value<std::string>(),
            "DB connection string")
        (OPT_FILE.c_str(), po::value<std::string>(),
            "input csv-file name (format: <id>,<speed-limit>)")
        (OPT_USER_ID.c_str(), po::value<revision::UserID>()->default_value(common::UID),
            "TDS committer")
        (OPT_BRANCH.c_str(), po::value<revision::DBID>()->default_value(0),
            "target branch id")
        (OPT_BATCH_SIZE.c_str(), po::value<size_t>()->default_value(BATCH_SIZE),
            "batch size for reading and commit")
        (OPT_BEFORE_ID.c_str(), po::value<revision::DBID>(),
            "skip input data for <id> >= <before-id>")
        (OPT_FORCE.c_str(),
            "start updating process if errors>0")
        (OPT_DRY_RUN.c_str(),
            "start process in simulation mode")
        (OPT_APPROVED_ONLY.c_str(),
            "skip update draft TDS data")
        (OPT_OVERWRITE.c_str(),
            "allow replacing data")
        (OPT_RESULT_COMMITS_FILE.c_str(), po::value<std::string>(),
            "result commits file name")
        (OPT_RESULT_DRAFT_OBJECTS_FILE.c_str(), po::value<std::string>(),
            "result draft objects file name")
        (OPT_RESULT_NON_EXISTENT_OBJECTS_FILE.c_str(), po::value<std::string>(),
            "result nonexistent objects file name");

    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);

    if (argc == 1 || vm.count(OPT_HELP)) {
        std::cerr << desc << std::endl;
        return 1;
    }

    try {
        run(vm);
        return 0;
    } catch(maps::Exception& ex) {
        ERROR() << ex;
    } catch(std::exception& ex) {
        ERROR() << ex.what();
    }
    return 2;
}
