#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/common/include/exception.h>
#include <maps/tools/grinder/worker/include/api.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/mongo/include/init.h>
#include <yandex/maps/mds/mds.h>
#include <maps/wikimap/mapspro/libs/acl/include/aclgateway.h>
#include <maps/wikimap/mapspro/libs/acl/include/exception.h>
#include <yandex/maps/wiki/common/batch.h>
#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/robot.h>
#include <yandex/maps/wiki/tasks/task_logger.h>
#include <yandex/maps/wiki/tasks/tasks.h>

#include <boost/algorithm/string/split.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <boost/lexical_cast.hpp>
#include <pqxx/pqxx>
#include <sstream>
#include <string>
#include <vector>

namespace worker = maps::grinder::worker;

namespace {

constexpr size_t USER_BATCH_SIZE = 100;
constexpr size_t WORKER_CONCURRENCY = 1;

} // anonymous namespace

namespace maps {
namespace wiki {
namespace badge {

namespace {

const std::string MDS_CONFIG_XPATH = "/config/common/mds";

enum class Action {
    Add,
    Remove
};

const std::string STR_ACTION_ADD = "add";
const std::string STR_ACTION_REMOVE = "remove";

const std::map<std::string, Action> STR_TO_ACTION = {
    {STR_ACTION_ADD, Action::Add},
    {STR_ACTION_REMOVE, Action::Remove}
};

Action stringToAction(const std::string& str)
{
    const auto it = STR_TO_ACTION.find(str);
    REQUIRE(it != STR_TO_ACTION.end(), "Invalid action: " + str);
    return it->second;
}

std::vector<uint64_t>
loadUidsFromFile(pqxx::transaction_base& txn, uint64_t taskId)
{
    std::ostringstream query;
    query << "SELECT file"
          << " FROM service.badge_task"
          << " WHERE id = " << taskId;

    auto rows = txn.exec(query.str());
    REQUIRE(!rows.empty(), "File for task " << taskId << " not found");
    const auto& row = rows[0];
    pqxx::binarystring binFile(row["file"]);

    std::vector<uint64_t> result;
    std::vector<std::string> splitUids;

    auto binFileStr = binFile.str();
    boost::split(splitUids, binFileStr,
        [](char c) {return c == '\r' or c == '\n';});

    for (auto str : splitUids) {
        boost::trim(str);
        if (str.empty()) {
            continue;
        }
        try {
            result.push_back(boost::lexical_cast<uint64_t>(str));
        } catch (const std::exception& e) {
            ERROR() << "Invalid user id: '" << str << "'";
            throw;
        }
    }
    return result;
}

bool isExistingBadge(pqxx::transaction_base& txn, const std::string& badgeId)
{
    std::ostringstream query;
    query << "SELECT 1"
          << " FROM social.badge_meta"
          << " WHERE badge_id = " << txn.quote(badgeId);

    auto rows = txn.exec(query.str());
    return !rows.empty();
}

void
addBadgeToUsers(
    pqxx::transaction_base& txn,
    const std::string& badgeId,
    const std::vector<uint64_t>& uids)
{
    std::ostringstream query;
    query << "INSERT INTO social.badge (uid, badge_id, awarded_by) VALUES "
          << common::join(uids, [&](auto uid) {
              return "(" + std::to_string(uid) + ", " + txn.quote(badgeId) +
                  ", " + std::to_string(common::ROBOT_UID) + ")";
          }, ",");

    txn.exec(query.str());
}

void
removeBadgeFromUsers(
    pqxx::transaction_base& txn,
    const std::string& badgeId,
    const std::vector<uint64_t>& uids)
{
    std::ostringstream query;
    query << "DELETE FROM social.badge WHERE uid IN "
          << "(" << common::join(uids, ",") << ")"
          << " AND badge_id = " << txn.quote(badgeId)
          // To avoid removing 'active years' or 'edit count' badges
          << " AND level IS NULL";

    txn.exec(query.str());
}

void
applyBadgeActionToUsers(
    pqxx::transaction_base& txn,
    Action action,
    const std::string& badgeId,
    const std::vector<uint64_t>& uids)
{
    if (action == Action::Add) {
        addBadgeToUsers(txn, badgeId, uids);
    } else {
        removeBadgeFromUsers(txn, badgeId, uids);
    }
}

mds::Configuration makeMdsConfig(const common::ExtendedXmlDoc& configXml)
{
    mds::Configuration mdsConfig(
        configXml.getAttr<std::string>(MDS_CONFIG_XPATH, "host"),
        configXml.getAttr<std::string>(MDS_CONFIG_XPATH, "namespace-name"),
        configXml.getAttr<std::string>(MDS_CONFIG_XPATH, "auth-header"));
    mdsConfig
        .setMaxRequestAttempts(3)
        .setRetryInitialTimeout(std::chrono::seconds(1))
        .setRetryTimeoutBackoff(2);
    return mdsConfig;
}

void
uploadResultLogToMds(
    const mds::Configuration& mdsConfig,
    std::istream& resultLog,
    pqxx::transaction_base& txn,
    uint64_t taskId)
{
    auto taskTag = tasks::makeTaskTag(taskId);
    auto path = "badge/" + taskTag + "/badge_" + taskTag + ".txt";

    mds::Mds mds(mdsConfig);
    auto response = mds.post(path, resultLog);
    auto url = mds.makeReadUrl(response.key());

    std::ostringstream query;
    query << "UPDATE service.badge_task"
          << " SET result_url = " << txn.quote(url)
          << " WHERE id = " << taskId;

    txn.exec(query.str());
}

} // anonymous namespace

void badgeWorker(
    const worker::Task& task,
    const common::ExtendedXmlDoc& configXml)
{
    const auto& args = task.args();
    auto taskId = args["taskId"].as<uint64_t>();
    auto actionStr = args["action"].as<std::string>();
    auto action = stringToAction(actionStr);
    auto badgeId = args["badgeId"].as<std::string>();

    INFO() << "Worker param 'action' = '" << actionStr << "'";
    INFO() << "Worker param 'badgeId' = '" << badgeId << "'";

    common::PoolHolder corePool(configXml, "core", "grinder");
    common::PoolHolder socialPool(configXml, "social", "grinder");
    tasks::TaskPgLogger logger(corePool.pool(), taskId);
    std::stringstream resultLog;

    logger.logInfo() << "Task started. Grinder task id: " << task.id();

    auto doWork = [&](pqxx::transaction_base& socialTxn) {
        REQUIRE(isExistingBadge(socialTxn, badgeId),
                "Badge '" << badgeId << "' does not exist");

        auto coreReadTxn = corePool.pool().slaveTransaction();
        auto uids = loadUidsFromFile(*coreReadTxn, taskId);
        REQUIRE(!uids.empty(), "Empty uids");

        acl::ACLGateway aclGateway(*coreReadTxn);
        std::vector<uint64_t> uidsToProcess;
        for (auto uid : uids) {
            try {
                auto userStatus = aclGateway.user(uid).status();
                switch (userStatus) {
                    case acl::User::Status::Active:
                        uidsToProcess.push_back(uid);
                        INFO() << "Badge applying:"
                               << " action ='" << actionStr << "'"
                               << " badge_id='" << badgeId << "'"
                               << " uid=" << uid;
                        break;
                    case acl::User::Status::Banned:
                        WARN() << "User id=" << uid << " is banned";
                        resultLog << uid << " : user in banned\n";
                        break;
                    case acl::User::Status::Deleted:
                        WARN() << "User id=" << uid << " is deleted";
                        resultLog << uid << " : user in deleted\n";
                        break;
                }
            } catch (const acl::UserNotExists&) {
                WARN() << "User id=" << uid << " does not exist";
                resultLog << uid << " : user does not exist\n";
                continue;
            }
        }

        common::applyBatchOp(uidsToProcess,
            USER_BATCH_SIZE,
            [&](const std::vector<uint64_t>& batchUids) {
                applyBadgeActionToUsers(socialTxn, action, badgeId, batchUids);
            });
    };

    try {
        auto socialWriteTxn = socialPool.pool().masterWriteableTransaction();

        doWork(*socialWriteTxn);

        resultLog.peek();
        if (!resultLog.eof()) {
            auto mdsConfig = makeMdsConfig(configXml);
            auto coreWriteTxn = corePool.pool().masterWriteableTransaction();
            uploadResultLogToMds(mdsConfig, resultLog, *coreWriteTxn, taskId);
            coreWriteTxn->commit();
        }
        socialWriteTxn->commit();
    } catch (const maps::Exception& e) {
        ERROR() << "Task failed: " << e;
        logger.logError() << "Task failed: " << e.what();
        throw;
    } catch (const std::exception& e) {
        ERROR() << "Task failed: " << e.what();
        logger.logError() << "Task failed: " << e.what();
        throw;
    }

    logger.logInfo() << "Task finished";
}

} // badge
} // wiki
} // maps

int main(int argc, char* argv[]) try
{
    maps::cmdline::Parser parser;
    auto workerConfig = parser.string("config")
            .help("path to worker configuration");
    auto syslogTag = parser.string("syslog-tag")
            .help("redirect log output to syslog with given tag");
    auto grinderConfig = parser.string("grinder-config")
            .help("path to grinder configuration file");

    parser.parse(argc, argv);

    if (syslogTag.defined()) {
        maps::log8::setBackend(maps::log8::toSyslog(syslogTag));
    }

    maps::mongo::init();

    std::unique_ptr<maps::wiki::common::ExtendedXmlDoc> configXmlPtr;
    if (workerConfig.defined()) {
        configXmlPtr.reset(new maps::wiki::common::ExtendedXmlDoc(workerConfig));
    } else {
        configXmlPtr = maps::wiki::common::loadDefaultConfig();
    }

    worker::Options workerOpts;

    if (grinderConfig.defined()) {
        workerOpts.setConfigLocation(grinderConfig);
    }

    workerOpts.on("badge",
        [&](const worker::Task& task) {
            maps::wiki::badge::badgeWorker(task, *configXmlPtr);
        }
    ).setConcurrencyLevel(WORKER_CONCURRENCY);

    worker::run(std::move(workerOpts));

    return EXIT_SUCCESS;
} catch (const maps::Exception& e) {
    FATAL() << "Worker failed: " << e;
    return EXIT_FAILURE;
} catch (const std::exception& e) {
    FATAL() << "Worker failed: " << e.what();
    return EXIT_FAILURE;
}
