#include <maps/wikimap/mapspro/services/gdpr/src/lib/worker.h>

#include <maps/wikimap/mapspro/services/gdpr/src/lib/utils.h>
#include <maps/wikimap/mapspro/services/gdpr/src/lib/sql_strings.h>

#include <maps/wikimap/mapspro/libs/acl/include/aclgateway.h>
#include <maps/wikimap/mapspro/libs/acl/include/exception.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/pg_utils.h>
#include <maps/wikimap/mapspro/libs/revision/include/yandex/maps/wiki/revision/commit.h>
#include <maps/wikimap/mapspro/libs/revision/include/yandex/maps/wiki/revision/revisionsgateway.h>
#include <maps/wikimap/mapspro/libs/social/include/yandex/maps/wiki/social/gateway.h>
#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/log8/include/log8.h>

namespace maps::wiki::gdpr {

namespace {

void execUpdate(
    pqxx::transaction_base& txn,
    const std::string& tableName,
    const std::string& query)
{
    auto r = txn.exec("UPDATE " + tableName + " " + query);
    INFO() << "Changed " << tableName << " rows: " << r.affected_rows();
}

void backup(
    pqxx::transaction_base& txn,
    TId takeoutId,
    const std::string& schema,
    const std::string& table,
    const std::string& column,
    auto id)
{
    INFO() << "Backuping " << schema << "." << table;

    std::ostringstream query;
    query <<
        "INSERT INTO gdpr_backup." << table <<
        " SELECT " << takeoutId << ", NOW(), * " <<
        " FROM " << schema << "." << table <<
        " WHERE " << column << " = " << id;

    auto r = txn.exec(query.str());
    INFO() << "Backup " << schema << "." << table << " rows: " << r.affected_rows();
}

void clean(
    pqxx::transaction_base& txn,
    const std::string& schema,
    const std::string& table,
    const std::string& column,
    auto id,
    const std::string& filter = {})
{
    INFO() << "Cleaning " << schema << "." << table;

    std::ostringstream query;
    query <<
        "DELETE FROM " << schema << "." << table <<
        " WHERE " << column << " = " << id;
    if (!filter.empty()) {
        query << " AND " << filter;
    }

    auto r = txn.exec(query.str());
    INFO() << "Cleaned " << schema << "." << table << " rows: " << r.affected_rows();
}

auto loadCurrentTakeoutData(IDbPools& dbPools, TUid uid)
{
    auto txnCore = dbPools.core().slaveTransaction();
    auto data = currentTakeoutData(*txnCore, uid);
    if (!data) {
        INFO() << "All takeouts already completed";
    }
    return data;
}

std::string dropUidFromMetadata(TId taskId, const std::string& originalTask)
{
    auto parsed = json::Value::fromString(originalTask);

    std::map<std::string, json::Value> values;
    for (const auto& field : parsed.fields()) {
        values.emplace(field, parsed[field]);
    }

    json::Builder builder;
    builder << [&](json::ObjectBuilder object) {
        for (const auto& [key, value] : values) {
            if (key != "metadata") {
                object[key] << value;
                continue;
            }
            object[key] << [&, &value=value](json::ObjectBuilder object) {
                for (const auto& field : value.fields()) {
                    if (field == "uid") {
                        INFO() << "Dropped uid " << value[field]
                               << " from sprav task: " << taskId;
                    } else {
                        object[field] << value[field];
                    }
                }
            };
        }
    };
    return builder.str();
}

std::optional<acl::User> tryGetUser(acl::ACLGateway& aclGw, TUid uid)
{
    try {
        return aclGw.user(uid);
    } catch (acl::UserNotExists) {
        return std::nullopt;
    }
}

} // namespace

Worker::Worker(IDbPools& dbPools, TUid uid, bool dryRun)
    : dbPools_(dbPools)
    , uid_(uid)
    , dryRun_(dryRun)
{
    REQUIRE(uid > 0, "Invalid uid: " << uid);
}

void Worker::start(const std::string& requestId) const
{
    auto txn = dbPools_.core().masterWriteableTransaction();
    auto data = createTakeoutData(*txn, uid_, requestId);
    INFO() << "New takeout id: " << data.takeoutId;
    finishTxn(*txn);
}

void Worker::status() const
{
    auto txn = dbPools_.core().slaveTransaction();
    auto state = currentTakeoutState(
        dbPools_.core(), dbPools_.social(), uid_);
    INFO() << "Status: " << state;
}

void Worker::info() const
{
    auto txn = dbPools_.core().masterReadOnlyTransaction();

    auto allData = getAllTakeoutData(*txn, uid_);
    INFO() << "Takeouts: " << allData.size();

    auto data = currentTakeoutData(*txn, uid_);
    if (data) {
        INFO() << "Takeout id: " << data->takeoutId << " in progress";
    } else if (!allData.empty()) {
        INFO() << "All takeouts completed";
    }
    for (const auto& data : allData) {
        INFO()
            << "Takeout id: " << data.takeoutId
            << " request id: '" << data.requestId << "'"
            << " rtime: '" << chrono::formatSqlDateTime(data.requestTime) << "'"
            << " ctime: '" <<
                (data.completedTime
                    ? chrono::formatSqlDateTime(*data.completedTime)
                    : "") << "'";
    }
    auto dataCounts = getCounts(dbPools_.core(), dbPools_.social(), uid_);
    INFO() << "User data counts, size: " << dataCounts.size();
    for (const auto& [key, count] : dataCounts) {
        INFO() << "Found data " << key << ": " << count;
    }
}

void Worker::anonymizeEdits() const
{
    auto txnCore = dbPools_.core().masterWriteableTransaction();
    auto data = currentTakeoutData(*txnCore, uid_);
    if (!data) {
        INFO() << "All takeouts already completed";
        return;
    }

    auto requestTimeStr = txnCore->quote(
        chrono::formatSqlDateTime(data->requestTime));
    INFO() << "Anonymizing edits before " << requestTimeStr;

    auto txnSocial = dbPools_.social().masterWriteableTransaction();

    std::ostringstream queryCommit;
    queryCommit <<
        "SET created_by = -created_by"
        " WHERE created_by = " << uid_ <<
        " AND created < " << requestTimeStr;
    execUpdate(*txnCore, sql::table::REVISION_COMMIT, queryCommit.str());

    std::ostringstream queryCommitEvent;
    queryCommitEvent <<
        "SET created_by = -created_by"
        " WHERE type='" << social::EventType::Edit << "'"
        " AND created_by = " << uid_ <<
        " AND created_at < " << requestTimeStr;
    execUpdate(*txnSocial, sql::table::SOCIAL_COMMIT_EVENT, queryCommitEvent.str());

    INFO() << "Anonymizing subscriptions";
    social::Gateway gtw(*txnSocial);
    auto console = gtw.subscriptionConsole(uid_);
    auto subscriptions = console.subscriptions();
    if (!subscriptions.empty()) {
        std::set<TId> feedIds;

        revision::RevisionsGateway gateway(*txnCore);
        auto snapshot = gateway.snapshot(gateway.headCommitId());
        for (const auto& subscription : subscriptions) {
            auto feedId = subscription.feedId();
            auto rev = snapshot.objectRevision(feedId);
            REQUIRE(rev, "Feed not found, id: " << feedId);
            auto commit = revision::Commit::load(*txnCore, rev->id().commitId());
            if (commit.createdBy() != uid_) {
                INFO() << "Anonymizing feed id: " << feedId;
                feedIds.emplace(feedId);
            }
        }
        if (!feedIds.empty()) {
            auto query =
                "SET subscriber = -subscriber"
                " WHERE " + common::whereClause("feed_id", feedIds);
            execUpdate(*txnSocial, sql::table::SOCIAL_SUBSCRIPTION, query);
        }
    }

    finishTxn(*txnSocial);
    finishTxn(*txnCore);
    INFO() << "Edits done";
}

void Worker::anonymizeComments() const
{
    auto data = loadCurrentTakeoutData(dbPools_, uid_);
    if (!data) {
        return;
    }

    auto txnSocial = dbPools_.social().masterWriteableTransaction();

    auto requestTimeStr = txnSocial->quote(
        chrono::formatSqlDateTime(data->requestTime));
    INFO() << "Anonymizing comments before " << requestTimeStr;

    std::ostringstream queryCommitEvent;
    queryCommitEvent <<
        "SET created_by = -created_by"
        " WHERE type IN ("
            "'" << social::EventType::Complaint << "', "
            "'" << social::EventType::RequestForDeletion << "')"
        " AND created_by = " << uid_ <<
        " AND created_at < " << requestTimeStr;
    execUpdate(*txnSocial, sql::table::SOCIAL_COMMIT_EVENT, queryCommitEvent.str());

    std::ostringstream queryComment;
    queryComment <<
        "SET created_by = -created_by"
        " WHERE created_by = " << uid_ <<
        " AND created_at < " << requestTimeStr;
    execUpdate(*txnSocial, sql::table::SOCIAL_COMMENT, queryComment.str());

    finishTxn(*txnSocial);
    INFO() << "Comments done";
}

void Worker::anonymizeFeedback() const
{
    auto data = loadCurrentTakeoutData(dbPools_, uid_);
    if (!data) {
        return;
    }

    auto txnSocial = dbPools_.social().masterWriteableTransaction();

    auto requestTimeStr = txnSocial->quote(
        chrono::formatSqlDateTime(data->requestTime));
    INFO() << "Anonymizing feedback before " << requestTimeStr;

    std::ostringstream queryCommitEvent;
    queryCommitEvent <<
        "SET created_by = -created_by"
        " WHERE type = '" << social::EventType::ClosedFeedback << "'"
        " AND created_by = " << uid_ <<
        " AND created_at < " << requestTimeStr;
    execUpdate(*txnSocial, sql::table::SOCIAL_COMMIT_EVENT, queryCommitEvent.str());

    std::ostringstream queryFeedbackEvent;
    queryFeedbackEvent <<
        "SET created_by = -created_by"
        " WHERE created_by = " << uid_ <<
        " AND created_at < " << requestTimeStr;
    execUpdate(*txnSocial, sql::table::SOCIAL_FEEDBACK_EVENT, queryFeedbackEvent.str());

    std::ostringstream queryFeedbackTask;
    queryFeedbackTask <<
        "SET resolved_by = -resolved_by"
        " WHERE resolved_by = " << uid_ <<
        " AND resolved_at < " << requestTimeStr;
    execUpdate(*txnSocial, sql::table::SOCIAL_FEEDBACK_TASK_OUTGOING_CLOSED, queryFeedbackTask.str());

    std::ostringstream queryFeedbackHistory;
    queryFeedbackHistory <<
        "SET modified_by = -modified_by"
        " WHERE modified_by = " << uid_ <<
        " AND modified_at < " << requestTimeStr;
    execUpdate(*txnSocial, sql::table::SOCIAL_FEEDBACK_HISTORY, queryFeedbackHistory.str());

    finishTxn(*txnSocial);
    INFO() << "Feedback done";
}

void Worker::anonymizeModeration() const
{
    auto data = loadCurrentTakeoutData(dbPools_, uid_);
    if (!data) {
        return;
    }

    auto txnSocial = dbPools_.social().masterWriteableTransaction();

    auto requestTimeStr = txnSocial->quote(
        chrono::formatSqlDateTime(data->requestTime));
    INFO() << "Anonymizing moderation before " << requestTimeStr;

    std::ostringstream queryTask;
    queryTask <<
        "SET resolved_by = -resolved_by"
        " WHERE resolved_by = " << uid_ <<
        " AND resolved_at < " << requestTimeStr;
    execUpdate(*txnSocial, sql::table::SOCIAL_TASK, queryTask.str());

    std::ostringstream queryTaskClosed;
    queryTaskClosed <<
        "SET closed_by = -closed_by"
        " WHERE closed_by = " << uid_ <<
        " AND closed_at < " << requestTimeStr;
    execUpdate(*txnSocial, sql::table::SOCIAL_TASK_CLOSED, queryTaskClosed.str());

    finishTxn(*txnSocial);
    INFO() << "Moderation done";
}

void Worker::anonymizeSprav() const
{
    auto data = loadCurrentTakeoutData(dbPools_, uid_);
    if (!data) {
        return;
    }

    auto txnSocial = dbPools_.social().masterWriteableTransaction();

    auto requestTimeStr = txnSocial->quote(
        chrono::formatSqlDateTime(data->requestTime));
    INFO() << "Anonymizing sprav tasks before " << requestTimeStr;

    std::map<TId, std::string> taskIdToOriginalTask;
    {
        std::ostringstream query;
        query <<
            "SELECT task_id, original_task"
            " FROM " << sql::table::SPRAV_TASKS <<
            " WHERE created_by = " << uid_ <<
            " AND created_at < " << requestTimeStr <<
            " ORDER BY task_id DESC FOR UPDATE";
        for (const auto& row : txnSocial->exec(query.str())) {
            auto taskId = row[0].as<TId>();
            taskIdToOriginalTask.emplace(
                taskId, dropUidFromMetadata(taskId, row[1].as<std::string>()));
        }
    }

    for (const auto& [taskId, originalTask] : taskIdToOriginalTask) {
        std::ostringstream query;
        query <<
            "SET created_by = -created_by, "
                "original_task = " << txnSocial->quote(originalTask) <<
            " WHERE task_id = " << taskId;
        execUpdate(*txnSocial, sql::table::SPRAV_TASKS, query.str());
    }
    finishTxn(*txnSocial);
    INFO() << "Sprav tasks done";
}

void Worker::cleanAclProfile() const
{
    auto txnCore = dbPools_.core().masterWriteableTransaction();
    auto data = currentTakeoutData(*txnCore, uid_);
    if (!data) {
        INFO() << "All takeouts already completed";
        return;
    }

    acl::ACLGateway aclGw(*txnCore);
    auto user = tryGetUser(aclGw, uid_);
    if (!user) {
        INFO() << "Can not find user " << uid_ << " in acl";
        return;
    }
    auto status = user->status();

    INFO() << "Cleaning acl profile, login: " << user->login() << " status: " << status;
    static const auto schema = "acl";

    auto backupTable = [&](const auto& table, const auto& column, auto id) {
        backup(*txnCore, data->takeoutId, schema, table, column, id);
    };

    backupTable(sql::table::ACL_BAN_RECORD, "br_uid", uid_);
    backupTable(sql::table::ACL_GROUP_USER, "user_id", user->id());
    backupTable(sql::table::ACL_POLICY, "agent_id", user->id());
    backupTable(sql::table::ACL_USER, "uid", uid_);

    INFO() << "Removing all policies";
    user->removePolicies();

    std::set<acl::ID> scheduleIds;
    const auto scheduledObjects = aclGw.scheduledObjectsForAgent(user->id());
    for (const auto& policy : scheduledObjects.policies) {
        scheduleIds.emplace(policy.schedule().id());
    }
    for (const auto& group : scheduledObjects.groups) {
        scheduleIds.emplace(group.schedule().id());
    }
    for (auto scheduleId : scheduleIds) {
        INFO() << "Drop schedules by id: " << scheduleId;
        aclGw.dropSchedulesObjects(scheduleId);
    }

    std::set<std::string> groups {"nmaps", "nmaps-novice"};
    for (const auto& group : user->groups()) {
        auto it = groups.find(group.name());
        if (it != groups.end()) {
            groups.erase(it);
        } else {
            INFO() << "Removing group: " << group.name();
            group.remove(*user);
        }
    }
    for (const auto& groupName : groups) {
        INFO() << "Adding group: " << groupName;
        aclGw.group(groupName).add(*user);
    };

    std::ostringstream query;
    query <<
        "SET created = NOW(), created_by = " << uid_ << ", "
            "modified = NOW(), modified_by = " << uid_;
    if (status != acl::User::Status::Banned) {
        query << ", current_ban_id = NULL";
    }
    query << " WHERE uid = " << uid_;
    execUpdate(*txnCore, "acl." + sql::table::ACL_USER, query.str());

    std::string banFilter;
    if (status == acl::User::Status::Banned) {
        auto ban = user->currentBan();
        ASSERT(ban);
        banFilter = "br_id != " + std::to_string(ban->id());
    }
    clean(*txnCore, schema, sql::table::ACL_BAN_RECORD, "br_uid", uid_, banFilter);

    INFO() << "Enqueue user cluster for user id: " << user->id();
    aclGw.enqueueUserCluster(user->id());

    finishTxn(*txnCore);
    INFO() << "Acl profile done";
}

void Worker::cleanSocialProfile() const
{
    auto data = loadCurrentTakeoutData(dbPools_, uid_);
    if (!data) {
        return;
    }

    INFO() << "Cleaning social profile";

    auto txnSocial = dbPools_.social().masterWriteableTransaction();

    static const auto schema = "social";
    static const auto column = "uid";

    for (const auto& table : sql::table::SOCIAL_PROFILE_TABLES) {
        backup(*txnSocial, data->takeoutId, schema, table, column, uid_);
        clean(*txnSocial, schema, table, column, uid_);
    }
    finishTxn(*txnSocial);
    INFO() << "Social profile done";
}

void Worker::cleanStats() const
{
    auto data = loadCurrentTakeoutData(dbPools_, uid_);
    if (!data) {
        return;
    }

    INFO() << "Cleaning stats";

    auto txnSocial = dbPools_.social().masterWriteableTransaction();

    static const auto schema = "social";
    static const auto column = "uid";

    for (const auto& table : sql::table::SOCIAL_STATS_TABLES) {
        backup(*txnSocial, data->takeoutId, schema, table, column, uid_);
        clean(*txnSocial, schema, table, column, uid_);
    }
    finishTxn(*txnSocial);
    INFO() << "Stats done";
}

void Worker::finish() const
{
    auto txn = dbPools_.core().masterWriteableTransaction();
    auto data = currentTakeoutData(*txn, uid_);
    if (!data) {
        INFO() << "All takeouts already completed";
        return;
    }

    INFO() << "Takeout id: " << data->takeoutId << " in progress";
    auto query =
        "UPDATE " + sql::table::GDPR_TAKEOUT +
        " SET " + sql::column::COMPLETED_AT + "=NOW()"
        " WHERE " + sql::column::TAKEOUT_ID + "=" + std::to_string(data->takeoutId) +
        " AND " + sql::column::COMPLETED_AT + " IS NULL";

    if (txn->exec(query).affected_rows() > 0) {
        INFO() << "Takeout id: " << data->takeoutId << " completed";
    }
    finishTxn(*txn);
}

void Worker::finishTxn(pqxx::transaction_base& txn) const
{
    dryRun_ ? txn.abort() : txn.commit();
}

} // namespace maps::wiki::gdpr
