#include "utils.h"

#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/shell_cmd.h>

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

#include <algorithm>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <locale>
#include <vector>

#include <boost/optional.hpp>
#include <boost/regex.hpp>

namespace fs = std::filesystem;

namespace maps {
namespace wiki {
namespace poi {
namespace {

const std::locale DEFAULT_LOCALE("");
const std::chrono::hours RESOURCE_TIMEOUT(72);
const std::string TASK_CODE_DATE_FORMAT = "%Y%m%d%H%M";
const std::string TASK_CODE_RE = "poi_(\\d{12})_\\d+";

bool isDigit(char c) { return std::isdigit(c, DEFAULT_LOCALE); }

boost::optional<chrono::TimePoint>
extractTimeFromResourceName(const std::string& resourceName)
{
    static const boost::regex PATTERN(TASK_CODE_RE);
    boost::smatch matchResult;
    if (boost::regex_match(resourceName, matchResult, PATTERN)
        && matchResult.size() == 2) {
        std::string date = matchResult[1];
        std::tm tm;
        memset(&tm, 0, sizeof(tm));
        strptime(date.c_str(), TASK_CODE_DATE_FORMAT.c_str(), &tm);
        return
        boost::optional<chrono::TimePoint>(
                chrono::TimePoint::clock::from_time_t(timegm(&tm))
            );
    }
    return boost::none;
}

std::vector<std::string> getOldSchemas(pgpool3::Pool& pool)
{
    std::vector<std::string> result;
    auto now = chrono::TimePoint::clock::now();
    auto rows = common::retryDuration([&] {
        auto txn = pool.masterReadOnlyTransaction();
        return txn->exec("SELECT nspname FROM pg_catalog.pg_namespace");
    });

    for (const auto& row : rows) {
        auto schema = row["nspname"].as<std::string>();
        auto time = extractTimeFromResourceName(schema);
        if (!time || (now - time.get()) < RESOURCE_TIMEOUT) {
            continue;
        }
        result.push_back(schema);
    }
    return result;
}

} // anonymous namespace

bool isValidPermalink(const std::string& permalink)
{
    return std::all_of(permalink.begin(), permalink.end(), isDigit);
}

std::string makeResourceName(revision::DBID approveOrder,
                             chrono::TimePoint timePoint)
{
    std::tm tm;
    auto epoch = chrono::TimePoint::clock::to_time_t(
        std::chrono::time_point_cast<std::chrono::system_clock::duration>(timePoint));
    gmtime_r(&epoch, &tm);

    char buf[sizeof("197001010000")];
    auto bytesWritten
        = ::strftime(buf, sizeof(buf), TASK_CODE_DATE_FORMAT.c_str(), &tm);
    REQUIRE(bytesWritten == sizeof(buf) - 1, "Failed to format timestamp");

    std::ostringstream os;
    os << "poi_" << buf << "_" << approveOrder;
    return os.str();
}

void cleanupOldDirs(const std::string tempDirPath)
{
    auto now = chrono::TimePoint::clock::now();
    fs::path p(tempDirPath);
    for (fs::directory_iterator it(p); it != fs::directory_iterator(); ++it) {
        auto& dir = it->path();
        if (!fs::is_directory(dir)) {
            continue;
        }
        auto time = extractTimeFromResourceName(dir.filename().string());
        if (!time || (now - time.get()) < RESOURCE_TIMEOUT) {
            continue;
        }
        try {
            fs::remove_all(dir);
            INFO() << "Removed dir '" << dir.string() << "'";
        }
        catch (const std::exception& e) {
            WARN() << "Failed to remove dir '" << dir.string()
                   << "': " << e.what();
        }
    }
}

void dropSchema(pgpool3::Pool& pool, const std::string& schema) try {
    common::retryDuration([&] {
        auto txn = pool.masterWriteableTransaction();
        txn->exec("DROP SCHEMA IF EXISTS " + schema + " CASCADE");
        txn->commit();
    });
    INFO() << "Dropped schema " << schema;
}
catch (const std::exception& e) {
    WARN() << "Failed to drop schema " << schema << ": " << e.what();
}

void cleanupOldSchemas(pgpool3::Pool& pool)
{
    for (const auto& schema : getOldSchemas(pool)) {
        dropSchema(pool, schema);
    }
}

void makeMd5(const std::string& sourceFile, const std::string& outputFile)
{
    std::ostringstream md5Cmd;
    md5Cmd << "md5sum -b " << sourceFile << " > " << outputFile;
    auto res = shell::runCmd(md5Cmd.str());
    REQUIRE(res.exitCode == 0, "md5 command failed: " << res.stdErr);
}

size_t fileSize(const std::string& filePath)
{
    std::ifstream in(filePath.c_str(),
                     std::ifstream::ate | std::ifstream::binary);
    return (size_t)in.tellg();
}

} // namespace poi
} // namespace wiki
} // namespace maps
