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

#include <maps/infra/yacare/include/limit_rate.h>
#include <maps/infra/yacare/include/params/tvm.h>
#include <maps/infra/yacare/include/tvm.h>
#include <maps/infra/yacare/include/yacare.h>

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

#include <yandex/maps/wiki/common/default_config.h>
#include <memory>

using namespace maps::wiki;

using TvmAlias = NTvmAuth::TClientSettings::TAlias;

namespace {

const TvmAlias TVM_ALIAS = "nmaps-gdpr";
const std::string NMAPS_ID = "nmaps_id";
const std::string NMAPS_SLUG = "nmaps_data";

struct Globals
{
public:
    maps::pgpool3::Pool& corePool() { return dbPools_.core(); }
    maps::pgpool3::Pool& socialPool() { return dbPools_.social(); }

    static Globals& instance()
    {
        auto& globals = instanceHolder();
        REQUIRE(globals, "Globals are not initialized");
        return *globals;
    }

    static void init(common::ExtendedXmlDoc& config)
    {
        instanceHolder().reset(new Globals(config));
    }

    static void reset()
    {
        instanceHolder().reset();
    }

private:
    explicit Globals(common::ExtendedXmlDoc& config)
        : dbPools_(config)
    {}

    static std::unique_ptr<Globals>& instanceHolder()
    {
        static std::unique_ptr<Globals> globals;
        return globals;
    }

    gdpr::DbPools dbPools_;
};

void makeError(yacare::Response& response, std::string_view message)
{
    response << YCR_JSON(obj) {
        obj["errors"] << [&](maps::json::ArrayBuilder b) {
            b << [&](maps::json::ObjectBuilder b) {
                b["code"] << "internal";
                b["message"] << message;
            };
        };
        obj["status"] << "error";
    };
}

std::optional<maps::chrono::TimePoint> getLastCompletedTime(gdpr::TUid uid)
{
    auto txnCore = Globals::instance().corePool().slaveTransaction();
    auto allData = gdpr::getAllTakeoutData(*txnCore, uid);
    if (allData.empty()) {
        return std::nullopt;
    }
    return allData.back().completedTime;
}

void makeResponseStatus(yacare::Response& response, gdpr::TUid uid)
{
    try {
        Globals& globals = Globals::instance();
        auto state = gdpr::currentTakeoutState(
            globals.corePool(), globals.socialPool(), uid);

        response << YCR_JSON(obj) {
            obj["data"] << [&](maps::json::ArrayBuilder b) {
                b << [&](maps::json::ObjectBuilder b) {
                    b["id"] = NMAPS_ID;
                    b["slug"] = NMAPS_SLUG;
                    b["state"] = toString(state);
                    if (state != gdpr::TakeoutState::DeleteInProgress) {
                        auto lastCompletedTime = getLastCompletedTime(uid);
                        if (lastCompletedTime) {
                            b["update_date"] =
                                maps::chrono::formatIsoDateTime(*lastCompletedTime);
                        }
                    }
                };
            };
            obj["status"] << "ok";
        };
    } catch (const maps::Exception& ex) {
        ERROR() << "Error on get status (uid:" << uid << "): " << ex;
        makeError(response, ex.what());
    } catch (const std::exception& ex) {
        ERROR() << "Error on get status (uid:" << uid << "): " << ex.what();
        makeError(response, ex.what());
    }
}

bool createTakeoutData(gdpr::TUid uid, const std::string& requestId)
{
    auto txnCore = Globals::instance().corePool().masterWriteableTransaction();
    if (gdpr::currentTakeoutData(*txnCore, uid)) {
        return false;
    }
    gdpr::createTakeoutData(*txnCore, uid, requestId);
    txnCore->commit();
    return true;
}

void makeResponseDelete(
    yacare::Response& response, gdpr::TUid uid, const std::string& requestId)
{
    try {
        if (!createTakeoutData(uid, requestId)) {
            makeError(response, "user data already in delete progress");
            return;
        }
        response << YCR_JSON(obj) {
            obj["status"] << "ok";
        };
    } catch (const maps::Exception& ex) {
        ERROR() << "Error on get status (uid:" << uid << "): " << ex;
        makeError(response, ex.what());
    } catch (const std::exception& ex) {
        ERROR() << "Error on get status (uid:" << uid << "): " << ex.what();
        makeError(response, ex.what());
    }
}

void enableTvm()
{
    INFO() << "Enable TVM suppport";
    auto tvmtoolSettings =
        maps::auth::TvmtoolSettings().selectClientAlias(TVM_ALIAS);
    yacare::tvm::configureUserAuth(tvmtoolSettings);
}

yacare::ThreadPool takeoutThreadPool("takeout", 8, 2048);

} // namespace


yacare::VirtualHost vhost {
    yacare::VirtualHost::SLB { "core-nmaps-gdpr" },
};

YCR_SET_DEFAULT(
    vhost,
    yacare::Tvm2ServiceRequire(TVM_ALIAS),
    yacare::LimitRate().resource("maps_core_nmaps_gdpr")
);

// The number of seconds before the shutdown to serve requests while not
// responding to /ping.
YCR_OPTIONS.shutdown().grace_period() = 10;

YCR_QUERY_CUSTOM_PARAM((), requestId, std::string, YCR_DEFAULT({}))
{ return yacare::impl::parseArg(dest, request, "request_id"); }

YCR_QUERY_PARAM(uid, gdpr::TUid);


YCR_USE(takeoutThreadPool) {

YCR_RESPOND_TO("GET /2/takeout/status/", requestId, userId)
{
    makeResponseStatus(response, userId);
}

YCR_RESPOND_TO("POST /2/takeout/delete/", requestId, userId)
{
    makeResponseDelete(response, userId, requestId);
}

} // YCR_USE

YCR_MAIN(argc, argv) try {
    maps::cmdline::Parser parser;
    auto config = parser.string("config")
        .help("path to wiki services config");

    parser.parse(argc, argv);

    auto configPtr = config.defined()
        ? std::make_unique<common::ExtendedXmlDoc>(config)
        : common::loadDefaultConfig();

    maps::log8::setLevel(configPtr->get<maps::log8::Level>(
        "/config/log/level", maps::log8::Level::INFO));

    enableTvm();

    Globals::init(*configPtr);
    yacare::run(yacare::RunSettings{.useSystemDefaultLocale = true});
    Globals::reset();

    return EXIT_SUCCESS;
} catch (maps::Exception& e) {
    ERROR() << e;
    return EXIT_FAILURE;
} catch (std::exception& e) {
    ERROR() << e.what();
    return EXIT_FAILURE;
}
