#include <yandex/maps/wiki/tasks/tasks.h>

#include <yandex/maps/wiki/common/retry_duration.h>

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

#include <boost/filesystem.hpp>

#include <ostream>
#include <sstream>
#include <ctime>
#include <cstdio>

namespace fs = boost::filesystem;

namespace maps::wiki::tasks {

namespace {

const std::string TASK_TABLE = "service.task";
const std::string TASK_TYPE_TABLE = "service.task_type";

const std::string CORE_DB_ID = "core";
const std::string CORE_POOL_ID = "core";

void setFrozenState(
        pqxx::transaction_base& txn,
        uint64_t taskId,
        FrozenState state)
{
    std::ostringstream cmd;
    cmd << "UPDATE " << TASK_TABLE << "\n"
        << "SET frozen=" << state << "\n"
        << "WHERE id=" << taskId;
    auto result = txn.exec(cmd.str());
    if (!result.affected_rows()) {
        throw TaskNotFoundException(taskId);
    }
}

} // anonymous namespace

std::ostream& operator<<(std::ostream& out, FrozenState state)
{
    if (state == FrozenState::NotFrozen)
        return out << "false";
    else if (state == FrozenState::Frozen)
        return out << "true";
    return out;
}

bool isFrozen(pqxx::transaction_base& txn, TaskId taskId)
{
    std::ostringstream cmd;
    cmd << "SELECT frozen\n"
        << "FROM " << TASK_TABLE << "\n"
        << "WHERE id=" << taskId;

    auto rows = txn.exec(cmd.str());
    if (rows.empty()) {
        throw TaskNotFoundException(taskId);
    }
    REQUIRE(!rows[0][0].is_null(),
            "task.frozen is unexpectedly null, taskId=" << taskId);
    return rows[0][0].as<bool>();
}

void freezeTask(pqxx::transaction_base& txn, TaskId taskId)
{
    setFrozenState(txn, taskId, FrozenState::Frozen);
}

void unfreezeTask(pqxx::transaction_base& txn, TaskId taskId)
{
    setFrozenState(txn, taskId, FrozenState::NotFrozen);
}

common::Attributes attributesForTaskType(
    pqxx::transaction_base& txn,
    const std::string& taskType)
{
    std::ostringstream query;
    query << "SELECT hstore_to_json(attributes) ";
    query << "FROM " << TASK_TYPE_TABLE << " ";
    query << "WHERE name = " << txn.quote(taskType);

    auto rows = txn.exec(query.str());
    if (rows.empty()) {
        return {};
    }
    return static_cast<common::Attributes&&>(json::Value::fromString(rows[0][0].as<std::string>()));
}

void concatenateAttributesForTaskType(
    pqxx::transaction_base& txn,
    const std::string& taskType,
    const common::Attributes& attributes)
{
    auto hstore = common::attributesToHstore(txn, attributes);
    if (hstore.empty()) {
        return;
    }

    std::ostringstream updateQuery;
    updateQuery << "UPDATE " << TASK_TYPE_TABLE << " ";
    updateQuery << "SET attributes = ";
    updateQuery << "attributes || " << hstore << " ";
    updateQuery << "WHERE name = " << txn.quote(taskType);

    auto result = txn.exec(updateQuery.str());
    if (!result.affected_rows()) {
        std::ostringstream insertQuery;
        insertQuery << "INSERT INTO " << TASK_TYPE_TABLE << " ";
        insertQuery << "VALUES ";
        insertQuery << "(" << txn.quote(taskType) << "," << hstore << ")";
        txn.exec(insertQuery.str());
    }
}

std::string makeTaskTag(TaskId taskId)
{
    auto now = std::time(nullptr);
    struct tm parts;
    localtime_r(&now, &parts);

    constexpr int BUF_LEN = 80;
    char buf[BUF_LEN];
    auto sz = snprintf(
            buf, BUF_LEN, "%04d%02d%02d_%06ld",
            1900 + parts.tm_year, 1 + parts.tm_mon, parts.tm_mday, taskId);
    REQUIRE(sz > 0 && sz < BUF_LEN, "Failed to generate task tag");

    return buf;
}

void deleteOldTmpFiles(const std::string& baseDir, chrono::Days days)
{
    auto now = std::chrono::system_clock::now();

    std::vector<fs::path> pathsToDelete;

    fs::directory_iterator end;
    for (fs::directory_iterator itr(baseDir); itr != end; ++itr) {
        auto delta = now - std::chrono::system_clock::from_time_t(fs::last_write_time(itr->path()));
        if (delta > days) {
            pathsToDelete.emplace_back(itr->path());
        }
    }

    for (const auto& path : pathsToDelete) {
        INFO() << "Delete old file " << path;
        fs::remove_all(path);
    }
}

bool setOverriddenStatus(
    pqxx::transaction_base& txn,
    TaskId taskId,
    TaskStatus taskStatus,
    bool force)
{
    REQUIRE(taskStatus != TaskStatus::Frozen, "Freezing tasks is not supported yet");

    auto doUpdate = [&]() {
        std::ostringstream updateQuery;
        updateQuery << "UPDATE service.task SET status = "
            << txn.quote(std::string(toString(taskStatus)))
            << " WHERE id = " << taskId;
        txn.exec(updateQuery.str());
        return true;
    };

    if (force) {
        return doUpdate();
    }

    std::ostringstream selectQuery;
    selectQuery << "SELECT status FROM service.task WHERE id = " << taskId << " FOR UPDATE";
    auto rows = txn.exec(selectQuery.str());
    if (rows.empty()) {
        throw TaskNotFoundException(taskId);
    }
    auto&& statusField = rows[0][0];
    switch (taskStatus)
    {
        case TaskStatus::InProgress:
            if (!statusField.is_null()) {
                return false;
            }
            break;

        case TaskStatus::Failed:
        case TaskStatus::Success:
            if (statusField.as<std::string>() != toString(TaskStatus::InProgress)) {
                return false;
            }
            break;

        case TaskStatus::Revoked:
            // Tasks are revoked from wiki-tasks servant
            // Here we just check that task is already revoked
            return statusField.as<std::string>() == toString(TaskStatus::Revoked);

        default:
            throw maps::LogicError("Unreachable code/not implemented");
    }
    return doUpdate();
}

bool setOverriddenStatus(
    pgpool3::Pool& pool,
    TaskId taskId,
    TaskStatus taskStatus,
    bool force)
{
    return common::retryDuration([&] {
        auto txn = pool.masterWriteableTransaction();
        bool ok = setOverriddenStatus(txn.get(), taskId, taskStatus, force);
        ok ? txn->commit() : txn->abort();
        return ok;
    });
}

bool setOverriddenStatus(
    const common::ExtendedXmlDoc& config,
    TaskId taskId,
    TaskStatus taskStatus,
    bool force)
{
    auto poolHolder = corePoolHolder(config);
    return setOverriddenStatus(poolHolder.pool(), taskId, taskStatus, force);
}

common::PoolHolder corePoolHolder(const common::ExtendedXmlDoc& config)
{
    return common::PoolHolder{config, CORE_DB_ID, CORE_POOL_ID};
}

std::optional<TaskStatus> getOverriddenStatus(pqxx::transaction_base& txn, TaskId taskId)
{
    std::ostringstream query;
    query << "SELECT status from service.task where id = " << taskId;
    auto rows = txn.exec(query.str());
    if (rows.empty()) {
        throw TaskNotFoundException(taskId);
    }
    return enum_io::tryFromString<TaskStatus>(rows[0][0].as<std::string>({}));
}

std::optional<TaskStatus> getOverriddenStatus(pgpool3::Pool& pool, TaskId taskId)
{
    return common::retryDuration([&] {
        auto txn = pool.masterReadOnlyTransaction();
        return getOverriddenStatus(txn.get(), taskId);
    });
}

} // namespace maps::wiki::tasks
