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

#include <maps/libs/json/include/value.h>

#include <maps/libs/st/include/issue.h>
#include <maps/libs/st/include/gateway.h>
#include <maps/libs/st/include/comment.h>
#include <maps/libs/st/include/configuration.h>

#include <maps/wikimap/mapspro/services/mrc/libs/toloka_client/include/yandex/maps/mrc/toloka_client/client.h>

#include <chrono>
#include <string>
#include <thread>

namespace st = maps::st;
namespace json = maps::json;
namespace toloka = maps::mrc::toloka::io;

namespace {

static const std::string ST_BASE_URL = "https://st-api.yandex-team.ru";

static const std::chrono::seconds TIMEOUT(10);
static const size_t MAX_REQUEST_ATTEMPTS = 6;
static const std::chrono::seconds RETRY_INITIAL_TIMEOUT(1);
static const double RETRY_TIMEOUT_BACKOFF = 2.;

// Extract pool id from json
// Json format:
// [
//   {
//     "poolId" : "asdf"
//   }
// ]
//
std::string getPoolIdFromJson(const std::string& jsonPath) {
    static const std::string FIELD_POOL_ID = "poolId";
    json::Value value = json::Value::fromFile(jsonPath);
    REQUIRE(value.size() == 1, "There can be only one pool in json");
    return value[0][FIELD_POOL_ID].as<std::string>();
}

// Check that pool is completed
// Pool is completed if it is closed and close reason is "COMPLETED"
bool isPoolCompleted(const toloka::Pool& pool) {
    return pool.status() == toloka::PoolStatus::Closed
        && pool.lastCloseReason() == toloka::PoolCloseReason::Completed;
}

// Check that pool is closed for not enough balance
// Pool is closed for not enough balance if it is closed
// and close reason is "NOT_ENOUGH_BALANCE"
bool isPoolClosedForNotEnoughBalance(const toloka::Pool& pool) {
    return pool.status() == toloka::PoolStatus::Closed
        && pool.lastCloseReason() == toloka::PoolCloseReason::NotEnoughBalance;
}

// Send notification to Startrek issue about low and increased balance
class BalanceNotifier {
public:
    BalanceNotifier(
        st::Gateway& startrekClient,
        const std::string& issueKey,
        double balanceThreshold,
        std::chrono::minutes notificationInterval)
        : startrekClient_(startrekClient)
        , issueKey_(issueKey)
        , balanceThreshold_(balanceThreshold)
        , notificationInterval_(notificationInterval)
    {}

    double balanceThreshold() const {
        return balanceThreshold_;
    }

    void hasLowBalance() {
        std::chrono::system_clock::time_point now = std::chrono::system_clock::now();

        bool needSendNotification = false;
        if (!lowBalanceTimePoint_.has_value()) {
            // send first notification
            needSendNotification = true;
        } else if (now > *lowBalanceTimePoint_ + notificationInterval_) {
            // resend notification
            needSendNotification = true;
        }

        if (needSendNotification) {
            startrekClient_.createComment(issueKey_, LOW_BALANCE_MESSAGE);
            lowBalanceTimePoint_ = now;
        }
    }

    void hasHighBalance() {
        if (lowBalanceTimePoint_.has_value()) {
            // balance is increased
            startrekClient_.createComment(issueKey_, HIGH_BALANCE_MESSAGE);
            lowBalanceTimePoint_.reset();
        }
    }

private:
    st::Gateway& startrekClient_;
    std::string issueKey_;
    double balanceThreshold_;
    std::chrono::minutes notificationInterval_;

    std::optional<std::chrono::system_clock::time_point> lowBalanceTimePoint_;

    static const std::string LOW_BALANCE_MESSAGE;
    static const std::string HIGH_BALANCE_MESSAGE;
};

const std::string BalanceNotifier::LOW_BALANCE_MESSAGE = "Средства в Я.Толоке заканчиваются. Пополните счет для продолжения валидации";
const std::string BalanceNotifier::HIGH_BALANCE_MESSAGE = "Баланс в Я.Толоке пополнен. Валидация продолжается";

} // namespace

int main(int argc, const char** argv)
try {
    maps::cmdline::Parser parser("Check Toloka balance and send notification");

    maps::cmdline::Option<std::string> issueKey = parser.string("st_issue")
        .required()
        .help("Key of issue (Example: TEST-123)");

    maps::cmdline::Option<std::string> startrekToken = parser.string("startrek_token")
        .required()
        .help("Startrek token without \"OAuth\" (see: https://auth.yandex-team.ru)");

    maps::cmdline::Option<std::string> poolJsonPath = parser.string("pool")
        .required()
        .help("Input json with pool id");

    maps::cmdline::Option<std::string> tolokaHost = parser.string("toloka_host")
        .required()
        .help("Toloka host: <sandbox.>toloka.yandex.ru");

    maps::cmdline::Option<std::string> tolokaToken = parser.string("toloka_token")
        .required()
        .help("Toloka token with \"OAuth\" (see: https://toloka.yandex.ru)");

    maps::cmdline::Option<double> balanceThreshold = parser.real("balance_threshold")
        .required()
        .help("Toloka balance is low if it is less than this value (in dollars)");

    maps::cmdline::Option<size_t> recheckIntervalInMinutes = parser.size_t("recheck_interval")
        .required()
        .help("Recheck toloka balance interval (in minutes)");

    maps::cmdline::Option<size_t> notificationIntervalInHours = parser.size_t("notification_interval")
        .required()
        .help("Notification about low balance interval (in hours)");

    parser.parse(argc, const_cast<char**>(argv));

    // Create Toloka client
    toloka::TolokaClient tolokaClient(tolokaHost, tolokaToken);
    tolokaClient.setTimeout(TIMEOUT)
                .setMaxRequestAttempts(MAX_REQUEST_ATTEMPTS)
                .setRetryInitialTimeout(RETRY_INITIAL_TIMEOUT)
                .setRetryTimeoutBackoff(RETRY_TIMEOUT_BACKOFF);

    // Create Startrek client
    maps::st::RetryPolicy retryPolicy;
    retryPolicy.setMaxRequestAttempts(MAX_REQUEST_ATTEMPTS);
    retryPolicy.setRetryInitialTimeout(RETRY_INITIAL_TIMEOUT);
    retryPolicy.setRetryTimeoutBackoff(RETRY_TIMEOUT_BACKOFF);

    maps::st::Configuration configuration(ST_BASE_URL, startrekToken);
    configuration.setRetryPolicy(retryPolicy);
    configuration.setTimeout(TIMEOUT);

    maps::st::Gateway startrekClient(configuration);

    // Get Toloka pool id
    std::string poolId = getPoolIdFromJson(poolJsonPath);

    // Create balace notifier
    std::chrono::hours notificationInterval(notificationIntervalInHours);
    BalanceNotifier balanceNotifier(
        startrekClient, issueKey, balanceThreshold, notificationInterval);

    // Check Toloka balance until pool is completed
    toloka::Pool pool = tolokaClient.getPool(poolId);
    std::chrono::minutes recheckInterval(recheckIntervalInMinutes);
    while (!isPoolCompleted(pool)) {
        toloka::Requester requester = tolokaClient.getRequester();
        if (requester.balance() < balanceNotifier.balanceThreshold()) {
            INFO() << "Requester " << requester.publicName().EN() << " has low balance";
            balanceNotifier.hasLowBalance();
        } else {
            INFO() << "Requester " << requester.publicName().EN() <<  " has high balance";
            balanceNotifier.hasHighBalance();
            if (isPoolClosedForNotEnoughBalance(pool)) {
                INFO() << "Reopen closed pool: " << poolId;
                tolokaClient.openPool(poolId); // reopen closed pool
            }
        }
        std::this_thread::sleep_for(recheckInterval);
        pool = tolokaClient.getPool(poolId);
    }

    return EXIT_SUCCESS;
}
catch (const maps::Exception& e) {
    INFO() << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    INFO() << e.what();
    return EXIT_FAILURE;
}
catch (...) {
    INFO() << "Caught unknown exception";
    return EXIT_FAILURE;
}
