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

#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/yt/common/include/utils.h>

#include <maps/libs/common/include/file_utils.h>

#include <mapreduce/yt/interface/client.h>

#include <regex>
#include <fstream>
#include <optional>
#include <utility>

constexpr const char* DATE = "date";
constexpr const char* DETECTION_RECALL = "detection_recall";
constexpr const char* DETECTION_PRECISION = "detection_precision";
constexpr const char* CLUSTERIZATION_RECALL = "clusterization_recall";
constexpr const char* CLUSTERIZATION_PRECISION = "clusterization_precision";

using Date = TString;

Date getMoscowDate() {
    static constexpr const char* DATE_FORMAT = "%Y-%m-%d";
    const maps::chrono::TimePoint time = maps::chrono::TimePoint::clock::now();
    const maps::chrono::TimePoint moscowTime = time + std::chrono::hours(3);
    return maps::chrono::formatIntegralDateTime(moscowTime, DATE_FORMAT).c_str();
}

class Quality {
public:
    static Quality fromFile(const std::string& path) {
        static const std::string FILE_TEMPLATE =
            "Detection:\n"
            "  precision: = ([0-9.]+)%\n"
            "  recall:    = ([0-9.]+)%\n"
            "Clusterization:\n"
            "  precision: = ([0-9.]+)%\n"
            "  recall:    = ([0-9.]+)%\n";

        std::string lines = maps::common::readFileToString(path);

        std::regex rgx(FILE_TEMPLATE);
        std::smatch matches;
        std::regex_search(lines, matches, rgx);
        REQUIRE(5 == matches.size(), "Failed to parse quality file");

        Quality quality;
        quality.detectionPrecision_ = std::stod(matches[1]);
        quality.detectionRecall_ = std::stod(matches[2]);
        quality.clusterizationPrecision_ = std::stod(matches[3]);
        quality.clusterizationRecall_ = std::stod(matches[4]);

        return quality;
    }

    NYT::TNode toNode() const {
        return NYT::TNode()
            (DETECTION_PRECISION, detectionPrecision_)
            (DETECTION_RECALL, detectionRecall_)
            (CLUSTERIZATION_PRECISION, clusterizationPrecision_)
            (CLUSTERIZATION_RECALL, clusterizationRecall_);
    }

private:
    double detectionPrecision_;
    double detectionRecall_;
    double clusterizationPrecision_;
    double clusterizationRecall_;
};

class QualityTable {
public:
    static QualityTable CreateIfNotExists(
        NYT::IClientPtr client,
        const std::string& tableName)
    {
        if (!client->Exists(NYT::TYPath(tableName))) {
            client->Create(NYT::TYPath(tableName), NYT::NT_TABLE,
                NYT::TCreateOptions()
                    .Recursive(true)
                    .Attributes(NYT::TNode()("schema", qualityTableSchema()))
            );
        }

        return QualityTable{client, tableName};
    }

    bool isUpToDate(const Date& currentDate) {
        std::optional<Date> lastDate = getLastDate();
        return lastDate.has_value()
            && lastDate.value() >= currentDate;
    }

    void addQuality(const Date& date, const Quality& quality) {
        NYT::TNode node = quality.toNode();
        node[DATE] = date;

        auto writer = client_->CreateTableWriter<NYT::TNode>(
            NYT::TRichYPath(tableName_).Append(true)
        );
        writer->AddRow(node);
        writer->Finish();
    }

private:
    QualityTable(NYT::IClientPtr client, const std::string& tableName)
        : client_(client)
        , tableName_(tableName)
    {}

    static NYT::TNode qualityTableSchema() {
        return NYT::TTableSchema()
            .AddColumn(DATE, NYT::EValueType::VT_STRING, NYT::ESortOrder::SO_ASCENDING)
            .AddColumn(DETECTION_PRECISION, NYT::EValueType::VT_DOUBLE)
            .AddColumn(DETECTION_RECALL, NYT::EValueType::VT_DOUBLE)
            .AddColumn(CLUSTERIZATION_PRECISION, NYT::EValueType::VT_DOUBLE)
            .AddColumn(CLUSTERIZATION_RECALL, NYT::EValueType::VT_DOUBLE).ToNode();
    }

    std::optional<Date> getLastDate() {
        auto row = maps::yt::getLastRecordFromTable<NYT::TNode>(client_, tableName_);
        if (!row.Defined()) {
            return std::nullopt;
        }
        return (*row)[DATE].AsString();
    }

    NYT::IClientPtr client_;
    NYT::TYPath tableName_;
};

int main(int argc, const char** argv) try {
    NYT::Initialize(argc, argv);

    maps::cmdline::Parser parser("Add quality to table");

    maps::cmdline::Option<std::string> tableName = parser.string("table-name")
        .required()
        .help("Path to YT table");

    maps::cmdline::Option<std::string> qualityPath = parser.string("quality")
        .required()
        .help("Path to text file with quality");

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


    QualityTable table = QualityTable::CreateIfNotExists(
        NYT::CreateClient("hahn"), tableName
    );

    Quality quality = Quality::fromFile(qualityPath);

    Date currentDate = getMoscowDate();

    if (!table.isUpToDate(currentDate)) {
        table.addQuality(currentDate, quality);
    } else {
        INFO() << "Quality table is up to date";
    }

    return EXIT_SUCCESS;
} catch (const maps::Exception& ex) {
    FATAL() << "Failed: " << ex;
    return EXIT_FAILURE;
} catch (const std::exception& ex) {
    FATAL() << "Failed: " << ex.what();
    return EXIT_FAILURE;
} catch (...) {
    FATAL() << "Unknown error!";
}
