#include "config.h"

#include "export_files.h"

#include <yandex/maps/wiki/common/pg_advisory_lock_ids.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/tasks/export.h>
#include <maps/libs/log8/include/log8.h>

#include <ctime>
#include <sstream>

namespace rev = maps::wiki::revision;
namespace fs = std::filesystem;

namespace maps::wiki::exporter {

const Region NO_REGION = "";

namespace {

const std::string JSON_2_YMAPSDF_MASSTRANSIT_XML_PATH =
    "/etc/yandex/maps/wiki/export_masstransit/json2ymapsdf.xml";

const std::string JSON_2_YMAPSDF_XML_PATH =
    "/etc/yandex/maps/wiki/export_ymapsdf/json2ymapsdf.xml";

const std::string JSON_2_YMAPSDF_TOOL = "json2ymapsdf";

const std::string SLAVE_STR = "slave";
const std::string MASTER_STR = "master";

std::optional<RelativeIdSource>
parseRelativeIdSource(const std::optional<std::string>& relativeIdSourceStr)
{
    if (!relativeIdSourceStr || relativeIdSourceStr->empty()) {
        return {};
    }
    auto parts = common::split(*relativeIdSourceStr, ",");
    REQUIRE(parts.size() == 3 &&
        (parts[0] == SLAVE_STR || parts[0] == MASTER_STR) &&
        !parts[1].empty() &&
        !parts[2].empty(),
        "relativeIdSource expected format '{master/slave},{role-id},{relative category id}' but got: " << *relativeIdSourceStr);
    return RelativeIdSource {
        .relationType = (parts[0] == SLAVE_STR ? RelatioType::ToSlave : RelatioType::ToMaster),
        .role = std::move(parts[1]),
        .relativeCategoryId = std::move(parts[2])
    };
}

CategoriesMap readCategories(
    const common::ExtendedXmlDoc& configXml,
    const std::string& categoriesNode)
{
    CategoriesMap serviceCategories;

    const auto categories =
        configXml.node(tasks::getConfigExportXpath() + "/" + categoriesNode).nodes("category");

    for (size_t i = 0; i < categories.size(); ++i) {
        const auto catName = categories[i].attr<std::string>("name");
        auto res = serviceCategories.emplace(catName, CategoryAttrNameToInfo());
        auto& categoryNameMap = res.first->second;
        const auto attributes = categories[i].nodes("attribute");
        for (size_t j = 0; j < attributes.size(); ++j) {
            auto relativeIdSourceStr = attributes[j].tryGetAttr<std::string>("relativeIdSource");
            categoryNameMap.emplace(
                    attributes[j].attr<std::string>("name"),
                    ServiceCategoryAttrInfo {
                        .type = attributes[j].attr<std::string>("type"),
                        .defVal = attributes[j].tryGetAttr<std::string>("default"),
                        .sourceAttrName = attributes[j].tryGetAttr<std::string>("source"),
                        .relativeIdSource = parseRelativeIdSource(relativeIdSourceStr)
                    });
        }
    }

    return serviceCategories;
}


CategoriesFiltersMap readCategoriesFilters(
    const common::ExtendedXmlDoc& configXml,
    const std::string& categoriesNode)
{
    CategoriesFiltersMap filters;

    const auto categories =
        configXml.node(tasks::getConfigExportXpath() + "/" + categoriesNode).nodes("category");

    for (size_t i = 0; i < categories.size(); ++i) {
        const auto catName = categories[i].attr<std::string>("name");
        auto&& filter = filters[catName];
        const auto attributes = categories[i].nodes("filter", true);
        for (size_t j = 0; j < attributes.size(); ++j) {
            auto attrName = attributes[j].attr<std::string>("attr");
            auto attrValue = attributes[j].attr<std::string>("value");
            filter.emplace_back(attrName, attrValue);
        }
    }
    return filters;
}


std::unique_ptr<common::PoolHolder> createPoolHolder(
    const common::ExtendedXmlDoc& configXml,
    const std::string& sectionName,
    const std::string& poolName)
{
    try {
        return std::unique_ptr<common::PoolHolder>(
            new common::PoolHolder(configXml, sectionName, poolName));
    } catch (const maps::xml3::NodeNotFound&) {
        return {};
    }
}


std::string regionSuffix(const Region& region)
{
    if (region == NO_REGION) {
        return {};
    }
    return "_" + region;
}

} // namespace


Regions readRegions(const common::ExtendedXmlDoc& configXml)
{
    Regions result;

    const auto regionsNode = configXml.node(tasks::getConfigExportXpath() + "/regions", true);
    if (!regionsNode.isNull()) {
        const auto regions = regionsNode.nodes("region", true);
        for (size_t regionIx = 0; regionIx < regions.size(); ++regionIx) {
            result.push_back(regions[regionIx].attr<std::string>("id"));
        }
    }

    if (result.empty()) {
        result.push_back(NO_REGION);
    }

    return result;
}

void updateMdsPath(mds::Configuration& mdsConfig, const Region& region)
{
    mdsConfig.setPathPrefix(mdsConfig.pathPrefix() + regionSuffix(region));
}

mds::Configuration makeMdsConfig(const common::ExtendedXmlDoc& configXml)
{
    std::vector<mds::Configuration> result;

    const auto MDS_XPATH = tasks::getConfigExportXpath() + "/mds";

    const int readPort = configXml.getAttr<int>(MDS_XPATH, "read-port", -1);
    const int writePort = configXml.getAttr<int>(MDS_XPATH, "write-port", -1);

    const auto host = configXml.getAttr<std::string>(MDS_XPATH, "host");
    const auto nsName = configXml.getAttr<std::string>(MDS_XPATH, "namespace-name");
    const auto authHeader = configXml.getAttr<std::string>(MDS_XPATH, "auth-header");

    auto datasetPath = configXml.getAttr<std::string>(
        MDS_XPATH, "dataset-path", DATASET_PATH_PREFIX);

    mds::Configuration mdsConfig(host, nsName, authHeader);

    mdsConfig.setPathPrefix(datasetPath)
        .setMaxRequestAttempts(10)
        .setRetryInitialTimeout(std::chrono::seconds(1))
        .setRetryTimeoutBackoff(2);

    if (readPort >= 0) {
        mdsConfig.setReadPort(readPort);
    }
    if (writePort >= 0) {
        mdsConfig.setWritePort(writePort);
    }

    return mdsConfig;
}


ExportConfig::ExportConfig(
        const TaskParams& taskParams,
        const common::ExtendedXmlDoc& configXml)
    : taskParams_(taskParams)
    , configXml_(configXml)
    , keepJson_(false)
    , mdsConfig_(makeMdsConfig(configXml))
    , schemaPrefix_(configXml.getAttr<std::string>(tasks::getConfigExportXpath(), "schema-prefix"))
    , mainPool_(configXml, "long-read", "export")
    , regions_({NO_REGION})
    , serviceCategories_(readCategories(configXml, "service-categories"))
    , mrcCategories_(readCategories(configXml, "mrc-categories"))
    , mrcFilters_(readCategoriesFilters(configXml, "mrc-categories"))
    , mrcPedestrianCategories_(readCategories(configXml, "mrc-pedestrian-categories"))
    , logger_(new exporter::DevNullLogger)
{
    tag_ = makeTaskTag(taskParams);
    mdsPool_ = createPoolHolder(configXml, "mds", "mds");
    transformCfg_ = (taskParams.subset == Subset::Masstransit)
        ? JSON_2_YMAPSDF_MASSTRANSIT_XML_PATH
        : JSON_2_YMAPSDF_XML_PATH;
    if (taskParams_.subset == Subset::Ymapsdf) {
        regions_ = readRegions(configXml);
    }
}


mds::Configuration ExportConfig::mdsConfig(const Region& region) const
{
    auto config = mdsConfig();
    updateMdsPath(config, region);
    return config;
}


std::string ExportConfig::schema(const Region& region) const
{
    return
        schemaPrefix_ +
        tag_ +
        regionSuffix(region);
}


std::string ExportConfig::json2ymapsdfPath() const
{
    return json2ymapsdfPath_
        ? *json2ymapsdfPath_
        : JSON_2_YMAPSDF_TOOL;
}


void ExportConfig::setLogger(std::unique_ptr<Logger> logger)
{
    REQUIRE(logger, "Logger is empty");
    logger_ = std::move(logger);
}


std::string ExportConfig::ymapsdfConnStr() const
{
    return dbLock_
        ? dbLock_->connStr()
        : tasks::defaultExportDatabase(configXml_);
}

void ExportConfig::detectAndLockNearestExportDatabase()
{
    ASSERT(!dbLock_);
    dbLock_ = tasks::detectAndLockNearestExportDatabase(
        configXml_, static_cast<int64_t>(common::AdvisoryLockIds::EXPORT_WORKER));
    REQUIRE(dbLock_, "Can not lock export database");
}

pgpool3::Pool& ExportConfig::ymapsdfPool() const
{
    if (!ymapsdfPool_) {
        ymapsdfPool_ = createPgPool(ymapsdfConnStr());
    }
    return *ymapsdfPool_;
}


std::ostream& operator<<(std::ostream& out, const ServiceCategoryAttrInfo& info)
{
    out << "{ type: " << info.type
        << ", defVal: " << (info.defVal ? *info.defVal : std::string("<none>"))
        << ", sourceAttrName: "
        << (info.sourceAttrName ? *info.sourceAttrName : std::string("<none>"))
        << " }";
    return out;
}


std::ostream& operator<<(std::ostream& out, const CategoryAttrNameToInfo& infos)
{
    out << common::join(infos, [](const std::pair<std::string, ServiceCategoryAttrInfo>& item){
            std::ostringstream os;
            os << "{ attr_name: " << item.first
               << " -> " << item.second
               << " }";
            return os.str();
        }, ", ");
    return out;
}


std::ostream& operator<<(std::ostream& out, const CategoriesMap& map)
{
    out << common::join(map, [](const std::pair<std::string, CategoryAttrNameToInfo>& item){
            std::ostringstream os;
            os << "{ category_name: " << item.first
               << " -> " << item.second
               << " }";
            return os.str();
        }, ", ");
    return out;
}


std::ostream& operator<<(std::ostream& out, const ExportConfig& cfg)
{
    out << "ExportConfig:\n";
    out << "    unique tag: " << cfg.tag() << "\n";
    out << "    schemaPrefix: " << cfg.schemaPrefix() << "\n";
    out << "    serviceCategories: " << cfg.serviceCategories() << "\n";
    out << "    mdsConfig: " << cfg.mdsConfig() << "\n";
    out << "    regions: "  << common::join(cfg.regions_, "; ") << "\n";
    out << "    experiment mode: " << (
        cfg.taskParams().experiment.empty()
        ? "[OFF]"
        : cfg.taskParams().experiment) << "\n";

    return out;
}

} // namespace maps::wiki::exporter
