#include "common.h"
#include "globals.h"

#include <maps/infra/yacare/include/params/tvm.h>
#include <maps/infra/yacare/include/request.h>
#include <maps/infra/yacare/include/tvm.h>
#include <maps/infra/yacare/include/yacare.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/sql_chemistry/include/exceptions.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/pg_locks.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/takeout_data_erasure_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/takeout_ongoing_job_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/async_takeout_data_erasure/lib/utility.h>
#include <maps/libs/common/include/exception.h>
#include <yandex/maps/pgpool3utils/pg_advisory_mutex.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>
#include <yandex/maps/wiki/common/string_utils.h>

#include <string>
#include <vector>

namespace maps::mrc::tasks_planner {

namespace {

const std::string BAD_REQUEST_ERROR = "bad_request";
const std::string INTERNAL_ERROR = "internal";
const std::string PHOTOS_CATEGORY_ID = "1";
const std::string PHOTOS_CATEGORY_SLUG = "photos";
const std::chrono::hours TAKEOUT_ONGOING_JOB_TIMEOUT{4};
const std::string TVM_ALIAS = "tasksplanner";

std::string makeErrorJson(const std::string& message)
{
    json::Builder builder;
    builder << [&](json::ObjectBuilder builder) {
        builder["status"] << "error";
        builder["error"] << message;
    };
    return builder.str();
}

std::pair<std::string, std::string> parseUidAndJobId(const std::string& body)
{
    yacare::QueryParams paramsMap;
    yacare::parseUrlencodedBody(body, &paramsMap);

    auto uidIt = paramsMap.find("uid");
    auto jobIdIt = paramsMap.find("job_id");

    if (uidIt == paramsMap.end() || jobIdIt == paramsMap.end()
        || uidIt->second.empty() || jobIdIt->second.empty()) {
        throw yacare::errors::BadRequest()
            << makeErrorJson("uid and job_id must be present in request");
    }
    return {uidIt->second.front(), jobIdIt->second.front()};
}

std::string startTakeoutOnGrinder(const std::string& uid, const std::string& jobId)
{
    INFO() << "Creating grinder task for takeout async uploading. "
               "job_id=" << jobId << ", uid=" << uid;

    auto grinderTaskId = Globals::grinderClient()
        .submit(
            json::Value({
                {"type", json::Value("mrc-async-takeout-uploader")},
                {"uid", json::Value(uid)},
                {"job_id", json::Value(jobId)},
                {"__idempotent", json::Value(true)}
            }));
    INFO() << "Created grinder job with id " << grinderTaskId;
    return grinderTaskId;
}

std::string loadDataErasureState(pgpool3::Pool& pool, const std::string& userId)
{
    if (auto takeoutDataErasure =
            db::TakeoutDataErasureGateway{*pool.slaveTransaction()}.tryLoadOne(
                db::table::TakeoutDataErasure::userId == userId,
                orderBy(db::table::TakeoutDataErasure::requestedAt).desc());
        takeoutDataErasure and not takeoutDataErasure->finishedAt()) {
        return "delete_in_progress";
    }
    else if (db::FeatureGateway{*pool.slaveTransaction()}.exists(
                 db::table::Feature::userId == userId and
                 not db::table::Feature::gdprDeleted.is(true))) {
        return "ready_to_delete";
    }
    else {
        return "empty";
    }
}

void startDataEraseOnGrinder(pgpool3::Pool& pool,
                             grinder::Client& grinderClient,
                             const std::string& requestId,
                             const std::string& userId)
{
    auto takeoutDataErasure = db::TakeoutDataErasure{
        requestId, userId, chrono::TimePoint::clock::now()};
    {
        auto txn = pool.masterWriteableTransaction();
        db::TakeoutDataErasureGateway{*txn}.insert(takeoutDataErasure);
        txn->commit();
    }

    INFO() << "Creating data erase grinder task for user " << userId;
    auto grinderTaskId = grinderClient.submit(json::Value(
        {{"type", json::Value(takeout_data_erasure::GRINDER_WORKER_TYPE)},
         {takeout_data_erasure::TAKEOUT_DATA_ERASURE_ID_FIELD,
          json::Value(takeoutDataErasure.id())},
         {"__idempotent", json::Value(true)}}));
    INFO() << "Created grinder data erase grinder task " << grinderTaskId;

    takeoutDataErasure.setGrinderTaskId(grinderTaskId);
    auto txn = pool.masterWriteableTransaction();
    db::TakeoutDataErasureGateway{*txn}.update(takeoutDataErasure);
    txn->commit();
}

std::string formatError(const std::string& code, const std::string& message)
{
    json::Builder bld;
    bld << [&](maps::json::ObjectBuilder bld) {
        bld["status"] = "error";
        bld["errors"] = [&](maps::json::ArrayBuilder bld) {
            bld << [&](maps::json::ObjectBuilder bld) {
                bld["code"] = code;
                bld["message"] = message;
            };
        };
    };
    return bld.str();
}

void requirePassportFrontendTvmId(auth::TvmId tvmId)
{
    const auto& tvmIds = Globals::passportFrontendTvmIds();
    if (std::find(tvmIds.begin(), tvmIds.end(), tvmId) == tvmIds.end()) {
        throw yacare::errors::Forbidden()
            << "Only passport frontend may perform this request";
    }
}

} // namespace


YCR_RESPOND_TO("POST /gdpr/takeout",
               YCR_USING(yacare::Tvm2ServiceRequire(TVM_ALIAS)))
{
    const auto [uid, jobId] = parseUidAndJobId(request.body());

    // First check if there is an already running Grinder task for this job_id
    maps::pgp3utils::PgAdvisoryXactMutex mutex{
        Globals::pool(),
        static_cast<int64_t>(maps::mrc::common::LockId::TakeoutUploader)};
    mutex.lock();
    auto txn = Globals::pool().masterWriteableTransaction();

    db::TakeoutOngoingJobGateway takeoutOngoingJobGtw{*txn};
    auto ongoingJob = takeoutOngoingJobGtw.tryLoadById(jobId);

    auto now = chrono::TimePoint::clock::now();

    // It is expected that the takeout uploader removes job record when done
    if (!ongoingJob
        || now - ongoingJob->startedAt() > TAKEOUT_ONGOING_JOB_TIMEOUT) {

        auto grinderTaskId = startTakeoutOnGrinder(uid, jobId);
        takeoutOngoingJobGtw.upsert(db::TakeoutOngoingJob{jobId, uid, grinderTaskId, now});
        txn->commit();
    }

    response << YCR_JSON(obj) { obj["status"] = "ok"; };
}

YCR_RESPOND_TO("GET /1/takeout/status/",
               request_id,
               userId,
               tvmId,
               YCR_USING(yacare::Tvm2ServiceRequire(TVM_ALIAS)))
{
    requirePassportFrontendTvmId(tvmId);
    try {
        auto state =
            loadDataErasureState(Globals::pool(), std::to_string(userId));
        json::Builder bld;
        bld << [&](maps::json::ObjectBuilder bld) {
            bld["status"] = "ok";
            bld["data"] << [&](maps::json::ArrayBuilder bld) {
                bld << [&](maps::json::ObjectBuilder bld) {
                    bld["id"] = PHOTOS_CATEGORY_ID;
                    bld["slug"] = PHOTOS_CATEGORY_SLUG;
                    bld["state"] = state;
                };
            };
        };
        response << bld.str();
    }
    catch (maps::Exception& exc) {
        ERROR() << exc;
        response << formatError(INTERNAL_ERROR, INTERNAL_ERROR);
    }
    catch (std::exception& exc) {
        ERROR() << exc.what();
        response << formatError(INTERNAL_ERROR, INTERNAL_ERROR);
    }
}

YCR_RESPOND_TO("POST /1/takeout/delete/",
               request_id,
               userId,
               tvmId,
               YCR_USING(yacare::Tvm2ServiceRequire(TVM_ALIAS)))
{
    requirePassportFrontendTvmId(tvmId);
    try {
        auto val = json::Value::fromString(request.body());
        if (!val.isObject()) {
            response << formatError(BAD_REQUEST_ERROR,
                                    "body must be json object");
            return;
        }
        auto ids = val["id"];
        if (ids.size() != 1 || !ids[0].isString() ||
            ids[0].as<std::string>() != PHOTOS_CATEGORY_ID) {
            response << formatError(
                BAD_REQUEST_ERROR, "body must contain json with valid data id");
            return;
        }

        startDataEraseOnGrinder(Globals::pool(),
                                Globals::grinderClient(),
                                request_id,
                                std::to_string(userId));

        json::Builder bld;
        bld << [](maps::json::ObjectBuilder bld) { bld["status"] = "ok"; };
        response << bld.str();
    }
    catch (maps::Exception& exc) {
        ERROR() << exc;
        response << formatError(INTERNAL_ERROR, INTERNAL_ERROR);
    }
    catch (std::exception& exc) {
        ERROR() << exc.what();
        response << formatError(INTERNAL_ERROR, INTERNAL_ERROR);
    }
}

}  // namespace maps::mrc::tasks_planner
