#include <maps/wikimap/mapspro/services/tasks_sprav/src/yang_downloader/lib/worker.h>
#include <maps/wikimap/mapspro/services/tasks_sprav/src/yang_downloader/lib/client.h>

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

#include <maps/libs/pgpool3utils/include/yandex/maps/pgpool3utils/pg_advisory_mutex.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/default_config.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/pg_advisory_lock_ids.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common//secrets.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/string_utils.h>
#include <maps/wikimap/mapspro/libs/tasks/include/runner.h>
#include <maps/wikimap/mapspro/libs/tasks/include/yandex/maps/wiki/tasks/status_writer.h>

#include <util/string/strip.h>
#include <util/system/env.h>
#include <filesystem>
#include <optional>
#include <set>

namespace fs = std::filesystem;

namespace maps::wiki::yang {
namespace {

constexpr std::chrono::seconds SLEEP_INTERVAL(5);
constexpr ProjectId YANG_PROJECT_ID = 11361;
const auto STATUS_FILENAME = "wiki-yang-downloader.status";
const auto ENVIRONMENT_YANG_TOKEN = "YANG_TOKEN";

std::string allActions()
{
    return common::join(maps::enum_io::enumerateValues<Action>(), ',');
}

std::set<Action> parseActions(const std::string& actions)
{
    std::set<Action> result;
    for (const auto& action : common::split(actions, ",")) {
        result.emplace(enum_io::fromString<Action>(action));
    }
    return result;
}

template <typename Option>
std::optional<std::string> makeStatusFilename(const Option& statusDir)
{
    std::optional<std::string> statusFilename;
    if (statusDir.defined()) {
        fs::path filepath(static_cast<std::string>(statusDir));
        filepath /= STATUS_FILENAME;
        statusFilename = filepath;
    }
    return statusFilename;
}

std::string getYangToken()
{
    std::string token = Strip(GetEnv(ENVIRONMENT_YANG_TOKEN));
    if (!token.empty()) {
        return token;
    }
    return common::secrets::tokenByKey(common::secrets::Key::RobotWikimapYangToken);
}

template <typename ErrorNotifier>
bool processClosedPools(
    Worker& worker,
    const std::set<Action>& actions,
    ErrorNotifier errorNotifier,
    size_t batchLimit)
{
    auto pendingPoolIds = worker.client().getClosedPoolIds();
    INFO() << "Loaded closed pools, size: " << pendingPoolIds.size();

    std::set<PoolId> processed;
    bool success = true;

    auto processOne = [&] {
        if (pendingPoolIds.empty()) {
            INFO() << "No more closed pools";
            return false;
        }
        auto poolId = pendingPoolIds.front();
        pendingPoolIds.pop_front();

        success = worker.run(actions, poolId);
        if (!success) {
            errorNotifier("Pool " + std::to_string(poolId) + " processing failed");
        } else if (!processed.emplace(poolId).second) {
            errorNotifier("Pool " + std::to_string(poolId) + " already processed");
            success = false;
        } else if (processed.size() < batchLimit) {
            return true; // ok, process next pool
        }
        return false; // stop procesing
    };

    tasks::Runner().run(
        [&] {
            if (processOne()) {
                INFO() << "Sleeping " << SLEEP_INTERVAL.count() << "s.";
            } else {
                tasks::Runner::stop();
            }
        },
        SLEEP_INTERVAL);

    INFO() << "Pools processed successfully: " << processed.size();
    return success;
}

int runWorker(
    const common::ExtendedXmlDoc& config,
    const std::string& tvmAlias,
    Client& client,
    const std::set<Action>& actions,
    const std::optional<std::string>& statusFilename,
    size_t batchLimit,
    PoolId poolId)
{
    if (batchLimit) {
        REQUIRE(actions.contains(Action::Upload) && actions.contains(Action::Archive),
                "Batch mode works with actions 'upload' and 'archive' together");
    }

    Worker worker(config, tvmAlias, client);

    maps::pgp3utils::PgAdvisoryXactMutex dbLocker(
        worker.database().socialPool(),
        static_cast<int64_t>(maps::wiki::common::AdvisoryLockIds::YANG_DOWNLOADER));
    if (!dbLocker.try_lock()) {
        INFO() << "Database is already locked. Downloader task interrupted.";
        return EXIT_SUCCESS;
    }

    if (!batchLimit) {
        return worker.run(actions, poolId)
            ? EXIT_SUCCESS
            : EXIT_FAILURE;
    }

    tasks::StatusWriter statusWriter(statusFilename);
    auto errorNotifier = [&](const std::string& error) {
        ERROR() << error;
        statusWriter.err(error);
    };

    try {
        auto result = processClosedPools(
            worker, actions, errorNotifier, batchLimit);
        statusWriter.flush();
        return result ? EXIT_SUCCESS : EXIT_FAILURE;
    } catch(const std::exception& ex) {
        errorNotifier(ex.what());
        statusWriter.flush();
    }
    return EXIT_FAILURE;
}

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

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

    maps::cmdline::Parser argsParser;
    auto config = argsParser.file("config")
        .help("Path to config.xml with database connection settings.");
    auto syslogTag = argsParser.string("syslog-tag")
        .help("Redirect log output to syslog with given tag");
    auto statusDir = argsParser.dir("status-dir")
        .help("Path to status dir");
    auto poolId = argsParser.size_t("pool-id")
        .help("Pool id from yang");
    auto actions = argsParser.string("actions")
        .help("Actions: " + allActions() + " (csv-list)")
        .defaultValue(std::string(toString(Action::Help)));
    auto tvmAlias = argsParser.string("tvm-alias")
        .help("TVM alias to access to MRC");
    auto projectId = argsParser.size_t("project-id")
        .help("Yang project id (default: " + std::to_string(YANG_PROJECT_ID) + ")")
        .defaultValue(YANG_PROJECT_ID);
    auto batchLimit = argsParser.size_t("batch-limit")
        .help("Process <N> closed pools in batch mode")
        .defaultValue(0);

    argsParser.parse(argc, argv);
    if (syslogTag.defined()) {
        maps::log8::setBackend(maps::log8::toSyslog(syslogTag));
    }

    auto actionSet = parseActions(actions);
    if (actionSet.contains(Action::Help)) {
        argsParser.printUsageAndExit(EXIT_FAILURE);
    }

    Client client(getYangToken(), projectId);
    if (actionSet.contains(Action::List)) {
        client.listClosedPools();
        return EXIT_SUCCESS;
    }

    if (!tvmAlias.defined() || (!poolId.defined() && !batchLimit)) {
        argsParser.printUsageAndExit(EXIT_FAILURE);
    }

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

    return runWorker(
        *configPtr,
        tvmAlias,
        client,
        actionSet,
        makeStatusFilename(statusDir),
        batchLimit,
        poolId);
} catch (const maps::Exception& e) {
    FATAL() << "Exception caught: " << e;
    return EXIT_FAILURE;
} catch (const std::exception& ex) {
    FATAL() << "Exception caught: " << ex.what();
    return EXIT_FAILURE;
} catch (...) {
    FATAL() << "Unknown exception caught";
    return EXIT_FAILURE;
}
