#include <yandex/maps/wiki/common/default_config.h>
#include <yandex/maps/wiki/common/extended_xml_doc.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>
#include <yandex/maps/wiki/common/string_utils.h>

#include <yandex/maps/wiki/revision/branch_manager.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <yandex/maps/wiki/validator/categories.h>
#include <yandex/maps/wiki/validator/validator.h>
#include <yandex/maps/wiki/validator/changed_objects.h>
#include <maps/wikimap/mapspro/services/tasks/validator-checks/splitter/checks_splitter.h>

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

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

#include <iostream>
#include <fstream>
#include <string>
#include <thread>
#include <vector>

namespace po = boost::program_options;

namespace maps::wiki {
namespace {

const std::string OPT_HELP = "help";
const std::string OPT_LIST = "list";
const std::string OPT_SPLIT = "split";
const std::string OPT_CONFIG = "config";
const std::string OPT_CHECKS = "checks";
const std::string OPT_ALL_CHECKS = "all-checks";
const std::string OPT_THREADS = "threads";
const std::string OPT_BRANCH = "branch";
const std::string OPT_COMMIT_ID = "commit-id";
const std::string OPT_AOI_IDS = "aoi-ids";
const std::string OPT_AOI_BUFEER = "aoi-buffer";
const std::string OPT_LOG_LEVEL = "log-level";
const std::string OPT_RESULT = "result";
const std::string OPT_ONLY_CHANGED_OBJECTS = "only-changed-objects";

const int EXITCODE_OK = 0;
const int EXITCODE_LOGIC_ERROR = 1;
const int EXITCODE_RUNTIME_ERROR = 2;

const std::string EDITOR_CONFIG_XPATH = "/config/services/editor/config";
const std::string AOI_COVERAGE_DIR = "/var/tmp/yandex-maps-mpro-validator-coverage.tool";

std::vector<std::string>
splitString(const std::string& str)
{
    if (str.empty()) {
        return {};
    }

    std::vector<std::string> parts;
    auto delimiter = [] (char c)
    {
        return c == ',';
    };
    boost::split(parts, str, delimiter, boost::algorithm::token_compress_on);
    return parts;
}

template <typename Type>
Type
getValue(const po::variables_map& vm, const std::string& option, Type defValue = {})
{
    if (!vm.count(option)) {
        return defValue;
    }
    try {
        return vm[option].as<Type>();
    } catch (...) {
        ERROR() << "invalid type of option: " << option;
        throw;
    }
}

typedef validator::TCheckId CheckId;
typedef std::set<CheckId> CheckIds;

std::unique_ptr<common::ExtendedXmlDoc> getConfig(const po::variables_map& vm)
{
    if (!vm.count(OPT_CONFIG)) {
        return common::loadDefaultConfig();
    }
    return std::make_unique<common::ExtendedXmlDoc>(
        getValue<std::string>(vm, OPT_CONFIG));
}

class Runner
{
public:
    explicit Runner(const po::variables_map& vm)
        : vm_(vm)
        , configDoc_(getConfig(vm))
        , validatorConfig_(configDoc_->get<std::string>(EDITOR_CONFIG_XPATH))
        , validator_(validatorConfig_)
    {
        validator_.initModules();

        for (const auto& module : validator_.modules()) {
            for (const auto& checkId : module.checkIds()) {
                allChecks_[module.name()].insert(checkId);
            }
        }
        checksSplitter_.reset(new validation::ChecksSplitter(validator_));
    }

    int run()
    {
        if (vm_.count(OPT_LIST)) {
            for (const auto& module2checks : allChecks_) {
                std::cout << module2checks.first << std::endl;
                for (const auto& checkId : module2checks.second) {
                    std::cout << "  " << checkId << " : "
                              << common::join(checksSplitter_->categories(checkId), ',')
                              << std::endl;
                }
            }
            return EXITCODE_OK;
        }

        REQUIRE(!vm_.count(OPT_ALL_CHECKS) || !vm_.count(OPT_CHECKS), "Ambiguous check arguments");

        CheckIds checks;
        if (vm_.count(OPT_CHECKS)) {
            for (const auto& checkId : splitString(getValue<std::string>(vm_, OPT_CHECKS))) {
                auto it = allChecks_.find(checkId);
                if (it != allChecks_.end()) {
                    checks.insert(it->second.begin(), it->second.end());
                } else {
                    checks.insert(checkId);
                }
            }
        } else if (vm_.count(OPT_ALL_CHECKS)) {
            for (const auto& [moduleId, checkIds] : allChecks_) {
                checks.insert(checkIds.begin(), checkIds.end());
            }
        }
        REQUIRE(!checks.empty(), "empty check ids");

        if (vm_.count(OPT_SPLIT)) {
            checksSplitter_->split(checks);
            return EXITCODE_OK;
        }

        common::PoolHolder longReadDbHolder(*configDoc_, "long-read", "validator.heavy");
        auto& pgPool = longReadDbHolder.pool();

        revision::DBID branchId = 0;
        auto commitId = getValue<revision::DBID>(vm_, OPT_COMMIT_ID);
        {
            auto work = pgPool.masterReadOnlyTransaction();
            auto branch = revision::BranchManager(*work).loadByString(
                getValue<std::string>(vm_, OPT_BRANCH));
            branchId = branch.id();
            if (!commitId) {
                commitId = revision::RevisionsGateway(*work, branch).headCommitId();
            }
        }

        auto threads = getValue<size_t>(vm_, OPT_THREADS);
        if (threads) {
            validator_.setCheckThreadsCount(threads);
        }

        INFO() << "Start validation (threads: " << threads << ")"
               << " branch: " << branchId
               << " commit: " << commitId;
        INFO() << "Checks: " << common::join(checks, ',');
        validator::ResultPtr result;

        REQUIRE(!vm_.count(OPT_AOI_IDS) || !vm_.count(OPT_ONLY_CHANGED_OBJECTS),
            "Changed objects can't be checked by aoi");

        if (!vm_.count(OPT_AOI_IDS)) {
            validator::ObjectIdSet objectIds;
            if (vm_.count(OPT_ONLY_CHANGED_OBJECTS)) {
                objectIds = validator::findChangedObjectsInReleaseBranch(pgPool, branchId, commitId);
            }
            result = validator_.run({checks.begin(), checks.end()}, pgPool, branchId, commitId, objectIds);
        } else {
            auto aoiIdsStr = splitString(getValue<std::string>(vm_, OPT_AOI_IDS));
            std::vector<revision::DBID> aoiIds;
            for (const auto& aoiIdStr : aoiIdsStr) {
                aoiIds.push_back(std::stoul(aoiIdStr));
            }
            result = validator_.run(
                {checks.begin(), checks.end()}, pgPool, branchId, commitId,
                aoiIds, AOI_COVERAGE_DIR, getValue<double>(vm_, OPT_AOI_BUFEER));
        }

        auto messages = result->drainAllMessages();
        if (vm_.count(OPT_RESULT)) {
            auto result_path = getValue<std::string>(vm_, OPT_RESULT);
            std::ofstream fs(result_path);
            REQUIRE(!fs.fail(), "couldn't open '" << result_path << "' for writing");

            for (const auto& message : messages) {
                const auto& attribs = message.attributes();
                std::vector<revision::DBID> objectIds;
                for (const auto& revisionId : message.revisionIds()) {
                    objectIds.push_back(revisionId.objectId());
                }
                fs << attribs.description << " " << attribs.severity << " " <<
                    common::join(objectIds, ",") << std::endl;
            }
        }

        INFO() << "Done";
        return EXITCODE_OK;
    }

private:
    const po::variables_map& vm_;
    std::unique_ptr<common::ExtendedXmlDoc> configDoc_;
    validator::ValidatorConfig validatorConfig_;
    validator::Validator validator_;
    std::unique_ptr<validation::ChecksSplitter> checksSplitter_;

    std::map<std::string, CheckIds> allChecks_;
};

} // namespace
} // namespace maps::wiki

int main(int argc, char** argv)
{
    using namespace maps::wiki;

    po::options_description desc("Usage: <tool> <options>\nOptions");
    desc.add_options()
        (OPT_HELP.c_str(),
            "show help message")
        (OPT_LIST.c_str(),
            "print list of check ids")
        (OPT_SPLIT.c_str(),
            "split check ids by groups")
        (OPT_CONFIG.c_str(), po::value<std::string>(),
            "config path (default: path to services.xml)")
        (OPT_CHECKS.c_str(), po::value<std::string>(),
            "comma separated checks ids")
        (OPT_ALL_CHECKS.c_str(),
            "run all checks")
        (OPT_THREADS.c_str(), po::value<size_t>()->default_value(
            std::thread::hardware_concurrency()),
            "working threads")
        (OPT_BRANCH.c_str(), po::value<std::string>()->default_value("trunk"),
            "branch id (trunk, approved, stable, <id>)")
        (OPT_COMMIT_ID.c_str(), po::value<revision::DBID>(),
            "commit id")
        (OPT_AOI_IDS.c_str(), po::value<std::string>(),
            "comma separated aoi ids (polygon or contour objects)")
        (OPT_AOI_BUFEER.c_str(), po::value<std::string>(),
            "size of buffer around aoi (in meters; default: 0.0)")
        (OPT_LOG_LEVEL.c_str(), po::value<std::string>(),
            "log level (debug|info|warn|error|fatal)")
        (OPT_RESULT.c_str(), po::value<std::string>(),
            "result file path")
        (OPT_ONLY_CHANGED_OBJECTS.c_str(),
            "check only changed objects in the release branch");

    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 EXITCODE_LOGIC_ERROR;
    }
    po::notify(vm);

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

    maps::log8::setLevel(getValue<std::string>(vm, OPT_LOG_LEVEL, "info"));

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