#include "csv_writer.h"

#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/attrdef.h>
#include <yandex/maps/wiki/configs/editor/role_filters.h>
#include <yandex/maps/wiki/common/retry_duration.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/tasks/tasks.h>
#include <yandex/maps/mds/mds.h>
#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/shell_cmd.h>
#include <yandex/maps/shellcmd/logging_ostream.h>

#include <geos/geom/GeometryFactory.h>

#include <sstream>
#include <fstream>

namespace fs = std::filesystem;

namespace maps::wiki::diffalert {

namespace sql {
namespace table {
const std::string DIFFALERT_TASK = "service.diffalert_task";
} // table
namespace column {
const std::string ID = "id";
const std::string RESULT_URL = "result_url";
} // column
} // sql

namespace {

const std::string BASE_DIR_XPATH = "/config/services/tasks/diffalert/base-dir";
const std::string MDS_CONFIG_XPATH = "/config/common/mds";

const std::string NO_AOI_NAME = "Other";

const std::string BYTE_ORDER_MARK = "\xef\xbb\xbf";

const std::vector<std::string> NAME_ROLES {
    "official",
    "render_label",
    "address_label",
    "short",
    "synonym",
    "old"
};

const std::string SYS_PREFIX = "sys:";

const std::string ATTR_LANG = "lang";
const std::string ATTR_NAME = "name";

struct NameWithLang
{
    NameWithLang(std::string name, std::string lang)
        : name(std::move(name))
        , lang(std::move(lang))
    {}

    std::string name;
    std::string lang;
};

using NameWithLangs = std::vector<NameWithLang>;
using RoleToNames = std::unordered_map<std::string, NameWithLangs>;

RoleToNames loadRoleToNames(Snapshot& snapshot, OptionalObject object)
{
    if (!object) {
        return {};
    }

    std::map<TId, std::string> nameIdToRole;
    TIds nameIds;

    for (const auto& rel : object->loadSlaveRelations()) {
        if (std::find(NAME_ROLES.begin(), NAME_ROLES.end(), rel.role) != NAME_ROLES.end()) {
            nameIds.insert(rel.slaveId);
            nameIdToRole.emplace(rel.slaveId, rel.role);
        }
    }

    RoleToNames roleToNames;

    auto nameObjects = snapshot.objectsByIds(nameIds);
    for (const auto& nameObject : nameObjects) {
        auto it = nameIdToRole.find(nameObject->id());
        ASSERT(it != nameIdToRole.end());
        const auto& roleId = it->second;

        roleToNames[roleId].emplace_back(
            nameObject->attr(ATTR_NAME).value(),
            nameObject->attr(ATTR_LANG).value());
    }

    return roleToNames;
};

fs::path compressFiles(
    const fs::path& baseDir,
    const fs::path& workDir,
    const std::string& tag)
{
    auto resultPath = baseDir / (tag + ".tar.gz");

    INFO() << "Compress files to " << resultPath.string();

    std::ostringstream os;
    os << "cd " << workDir << " && "
       << "tar --create --gzip --file " << resultPath << " *";

    auto command = os.str();
    INFO() << "Tar/Gzip command is '" << command << "'";

    auto result = shell::runCmd(std::move(command));
    REQUIRE(result.exitCode == 0, "Fail: " << result.stdErr);
    return resultPath;
}

std::string uploadToMds(
    const common::ExtendedXmlDoc& config,
    const fs::path& resultPath,
    const std::string& tag)
{
    auto mdsPath = "diffalert/" + tag + "/diffalert_results_" + tag + ".tar.gz";

    INFO() << "Upload tar.gz archive to " << mdsPath;

    mds::Configuration mdsConfig(
        config.getAttr<std::string>(MDS_CONFIG_XPATH, "host"),
        config.getAttr<std::string>(MDS_CONFIG_XPATH, "namespace-name"),
        config.getAttr<std::string>(MDS_CONFIG_XPATH, "auth-header"));
    mdsConfig
        .setMaxRequestAttempts(3)
        .setRetryInitialTimeout(std::chrono::seconds(1))
        .setRetryTimeoutBackoff(2);

    return common::retryDuration([&] {
        mds::Mds mds(mdsConfig);

        std::ifstream file(resultPath.string(), std::ios::in | std::ios::binary);
        REQUIRE(file, "Failed to open file for reading '" << resultPath.string() << "'");
        auto response = mds.post(mdsPath, file);
        file.close();

        return mds.makeReadUrl(response.key());
    });
}

void updateDb(
    int64_t taskId,
    const std::string& url,
    pgpool3::Pool& corePool)
{
    INFO() << "Write result_url '" << url << "' to db";

    common::retryDuration([&] {
        auto txn = corePool.masterWriteableTransaction();
        std::ostringstream query;
        query << "UPDATE " << sql::table::DIFFALERT_TASK
            << " SET " << sql::column::RESULT_URL << "=" << txn->quote(url)
            << " WHERE " << sql::column::ID << "=" << taskId;
        txn->exec(query.str());
        txn->commit();
    });
}

} // namespace

CsvCategoryWriter::CsvCategoryWriter(
        const configs::editor::Category& category,
        const std::vector<Aoi>& aois)
    : category_(category)
    , aois_(aois)
{
    for (const auto& attrDef : category_.attrDefs()) {
        if (attrDef->table() || attrDef->system()) {
            continue;
        }
        if (attrDef->id().starts_with(SYS_PREFIX)) {
            continue;
        }
        attrIds_.push_back(attrDef->id());
    }
}

void CsvCategoryWriter::put(
    const LongtaskDiffContext& diffContext,
    const std::list<StoredMessage>& messages)
{
    std::vector<std::string> objectCsvRow;

    pushAttributes(diffContext, objectCsvRow);
    if (category_.hasNames()) {
        pushNames(diffContext, objectCsvRow);
    }

    std::lock_guard<std::mutex> guard(mutex_);

    objectIdToCsvRow_.emplace(diffContext.objectId(), std::move(objectCsvRow));

    for (const auto& message : messages) {
        common::Geom messageGeom(
            geos::geom::GeometryFactory::getDefaultInstance()->toGeometry(&message.envelope()));

        bool aoiFound = false;

        for (const auto& aoi : aois_) {
            if (aoi.geom->intersects(messageGeom.geosGeometryPtr())) {
                aoiToDescriptionToMessages_[aoi.name][message.description()].push_back(message);
                aoiFound = true;
            }
        }

        if (!aoiFound) {
            aoiToDescriptionToMessages_[NO_AOI_NAME][message.description()].push_back(message);
        }
    }
}

void CsvCategoryWriter::pushAttributes(
    const LongtaskDiffContext& diffContext,
    std::vector<std::string>& objectCsvRow) const
{
    for (const auto& attrId : attrIds_) {
        if (diffContext.oldObject()) {
            objectCsvRow.push_back(diffContext.oldObject()->attr(attrId).value());
        } else {
            objectCsvRow.emplace_back();
        }
        if (diffContext.newObject()) {
            objectCsvRow.push_back(diffContext.newObject()->attr(attrId).value());
        } else {
            objectCsvRow.emplace_back();
        }
    }
}

void CsvCategoryWriter::pushNames(
    const LongtaskDiffContext& diffContext,
    std::vector<std::string>& objectCsvRow) const
{
    ASSERT(category_.hasNames());

    auto pushRole = [&](const RoleToNames& roleToNames, const std::string& roleId) {
        auto it = roleToNames.find(roleId);
        if (it == roleToNames.end()) {
            objectCsvRow.emplace_back();
            objectCsvRow.emplace_back();
        } else {
            objectCsvRow.push_back(common::join(
                it->second,
                [](const NameWithLang& nameWithlang){ return "\"" + nameWithlang.name + "\""; },
                ", "));
            objectCsvRow.push_back(common::join(
                it->second,
                [](const NameWithLang& nameWithlang){ return nameWithlang.lang; },
                ", "));
        }
    };

    auto oldRoleToNames = loadRoleToNames(diffContext.oldSnapshot(), diffContext.oldObject());
    auto newRoleToNames = loadRoleToNames(diffContext.newSnapshot(), diffContext.newObject());

    for (const auto& roleId : NAME_ROLES) {
        pushRole(oldRoleToNames, roleId);
        pushRole(newRoleToNames, roleId);
    }
}

void CsvCategoryWriter::writeTo(const std::filesystem::path& workDir)
{
    for (const auto& pair : aoiToDescriptionToMessages_) {
        const auto& aoiName = pair.first;
        const auto& descriptionToMessages = pair.second;

        for (const auto& pair : descriptionToMessages) {
            const auto& description = pair.first;

            fs::create_directories(workDir / aoiName);
            auto filepath = workDir / aoiName / (description + "_" + category_.id() + ".csv");

            std::ofstream file(filepath.string(), std::ios::out | std::ios::binary);
            REQUIRE(file, "Failed to open file for writing '" << filepath.string() << "'");

            /// A few hacks to make opening CSV files in Excel easier:
            /// * Add BOM at the start of the file so it can recognize UTF8.
            /// * Use ',' as decimal point separator (the default in Windows with Russian locale).
            /// * Use ';' as a CSV field separator.
            file << BYTE_ORDER_MARK;

            csv::OutputStream csvStream(file, ';');
            writeCsvHeader(csvStream);

            for (const auto& message : pair.second) {
                std::ostringstream priorityStream;
                priorityStream << message.priority().major << "," << message.priority().minor;

                csvStream
                    << message.categoryId()
                    << message.description()
                    << priorityStream.str()
                    << message.objectId();

                auto it = objectIdToCsvRow_.find(message.objectId());
                REQUIRE(it != objectIdToCsvRow_.end(), "Object id '" << message.objectId() << "' is not found");
                const auto& csvRow = it->second;
                for (const auto& value : csvRow) {
                    csvStream << value;
                }

                csvStream << csv::endl;
            }

            file.close();
        }
    }
}

void CsvCategoryWriter::writeCsvHeader(csv::OutputStream& csvStream) const
{
    csvStream
        << "category_id"
        << "description"
        << "priority"
        << "object_id";

    auto stripPrefix = [](const std::string& attrId) -> std::string {
        auto pos = attrId.find(":");
        if (pos != std::string::npos) {
            return attrId.substr(pos + 1);
        }
        return attrId;
    };

    for (const auto& attrId : attrIds_) {
        auto fixedAttrId = stripPrefix(attrId);
        csvStream << fixedAttrId + "_old";
        csvStream << fixedAttrId + "_new";
    }

    if (category_.hasNames()) {
        for (const auto& roleId : NAME_ROLES) {
            csvStream << roleId + "_name_old";
            csvStream << roleId + "_lang_old";
            csvStream << roleId + "_name_new";
            csvStream << roleId + "_lang_new";
        }
    }

    csvStream << csv::endl;
}

//================================================================

CsvWriter::CsvWriter(
        const configs::editor::ConfigHolder& editorConfig,
        std::vector<Aoi> aois)
    : editorConfig_(editorConfig)
    , aois_(std::move(aois))
{
    for (const auto& pair : editorConfig_.categories()) {
        const auto& category = pair.second;
        categoryToWriter_.emplace(
            std::piecewise_construct,
            std::tie(category.id()),
            std::tie(category, aois_));
    }
}

void CsvWriter::put(const LongtaskDiffContext& diffContext, const std::list<StoredMessage>& messages)
{
    if (!diffContext.attrsChanged() && !diffContext.tableAttrsChanged()) {
        return;
    }

    auto it = categoryToWriter_.find(diffContext.categoryId());
    REQUIRE(it != categoryToWriter_.end(), "Unexpected category '" << diffContext.categoryId() << "'");
    it->second.put(diffContext, messages);
}

void CsvWriter::publishToMds(
        int64_t taskId,
        const common::ExtendedXmlDoc& config,
        pgpool3::Pool& corePool,
        tasks::TaskPgLogger& taskLogger)
{
    auto tag = tasks::makeTaskTag(taskId);
    fs::path baseDir = config.get<std::string>(BASE_DIR_XPATH);
    auto workDir = baseDir / tag;
    fs::create_directories(workDir);

    INFO() << "Created work dir " << workDir.string();
    INFO() << "Write csv files";

    for (auto& pair : categoryToWriter_) {
        auto& writer = pair.second;
        writer.writeTo(workDir);
    }

    if (fs::is_empty(workDir)) {
        INFO() << "No diffalert messages found";
        taskLogger.logInfo() << "No diffalert messages found";
        return;
    }

    auto resultPath = compressFiles(baseDir, workDir, tag);

    try {
        auto url = uploadToMds(config, resultPath, tag);
        updateDb(taskId, url, corePool);
    } catch (const maps::Exception& ex) {
        ERROR() << "Failed to upload CSV: " << ex;
        taskLogger.logError() << "Failed to upload archive with CSV " << resultPath;
    } catch (const std::exception& ex) {
        ERROR() << "Failed to upload CSV: " << ex.what();
        taskLogger.logError() << "Failed to upload archive with CSV " << resultPath;
    }

    tasks::deleteOldTmpFiles(baseDir.string());
}

} // maps::wiki::diffalert
