#include <yandex/maps/wiki/common/default_config.h>
#include <yandex/maps/wiki/common/extended_xml_doc.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>
#include <yandex/maps/wiki/common/tskv_logger.h>
#include <yandex/maps/wiki/common/pg_advisory_lock_ids.h>

#include <yandex/maps/wiki/revision/filters.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>

#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/linear_ring.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/pgpool3utils/pg_advisory_mutex.h>

#include <fstream>
#include <string>
#include <unordered_set>
#include <vector>

namespace gl = maps::geolib3;
namespace common = maps::wiki::common;
namespace revision = maps::wiki::revision;
namespace rf = maps::wiki::revision::filters;

namespace {

const std::string DB_ID = "core";
const std::string POOL_ID = "grinder";

const std::string STAT_OUTPUT_FILE =
    "/var/lib/yandex/maps/wiki/stat/outsource-regions-dump.tskv";

const std::string STAT_LOG_FORMAT = "nmaps-outsource-regions-dump-log";

const std::string CATEGORY_PREFIX = "outsource_region:";

const std::unordered_set<std::string> ATTRS_TO_DUMP = {
    "name",
    "task_type",
    "fc_set",
    "complexity_rate",
    "company_id",
    "outsourcer_login",
    "parent_st_key",
    "status",
    "quality"
};

std::string wkbToGeoWkt(const std::string& wkb)
{
    gl::PointsVector points;

    std::stringstream stream(wkb);
    auto polygon = gl::WKB::read<gl::Polygon2>(stream);
    auto exterior = polygon.exteriorRing();
    for (size_t i = 0; i < exterior.pointsNumber(); i++) {
        points.push_back(gl::mercator2GeoPoint(exterior.pointAt(i)));
    }

    return gl::WKT::toString(gl::Polygon2(points));
}

std::vector<common::TskvMessage> createMessages(pqxx::transaction_base& txn)
{
    auto now = std::to_string(maps::chrono::sinceEpoch<std::chrono::seconds>());

    revision::RevisionsGateway gateway(txn);
    auto snapshot = gateway.snapshot(gateway.headCommitId());

    rf::ProxyFilterExpr filter =
        rf::Attr("cat:outsource_region").defined()
        && rf::Geom::defined()
        && rf::ObjRevAttr::isNotRelation()
        && rf::ObjRevAttr::isNotDeleted();

    std::vector<common::TskvMessage> messages;

    for (const auto& rev : snapshot.objectRevisionsByFilter(filter)) {
        auto objectId = rev.id().objectId();

        common::TskvMessage message(STAT_LOG_FORMAT);
        message.setParam("object_id", std::to_string(objectId));

        const auto& attributes = *rev.data().attributes;
        for (const auto& attrName : ATTRS_TO_DUMP) {
            auto it = attributes.find(CATEGORY_PREFIX + attrName);
            message.setParam(attrName,
                it != attributes.end()
                ? it->second
                : std::string{});
        }

        message.setParam("geometry", wkbToGeoWkt(*rev.data().geometry));

        message.setParam("unixtime", now);

        messages.push_back(std::move(message));
    }

    return messages;
}

void writeMessages(const std::vector<common::TskvMessage>& messages)
{
    std::ofstream outStream(STAT_OUTPUT_FILE, std::ios_base::app);
    REQUIRE(outStream, "Error opening output file \"" << STAT_OUTPUT_FILE << '"');

    for (const auto& message : messages) {
        outStream << message.toString() + "\n";
    }

    outStream.close();
}

} // namespace

int main(int argc, char** argv)
{
    try {
        maps::cmdline::Parser parser;

        auto syslogTag = parser.string("syslog-tag").help(
            "redirect log output to syslog with given tag");
        auto workerConfig = parser.file("config").help(
            "path to worker configuration");

        parser.parse(argc, argv);

        if (syslogTag.defined()) {
            maps::log8::setBackend(maps::log8::toSyslog(syslogTag));
        }

        std::unique_ptr<common::ExtendedXmlDoc> configDocPtr;
        if (workerConfig.defined()) {
            configDocPtr.reset(new common::ExtendedXmlDoc(workerConfig));
        } else {
            configDocPtr = common::loadDefaultConfig();
        }

        common::PoolHolder poolHolder(*configDocPtr, DB_ID, POOL_ID);

        maps::pgp3utils::PgAdvisoryXactMutex locker(
            poolHolder.pool(),
            static_cast<int64_t>(common::AdvisoryLockIds::STAT_OUTSOURCE_REGIONS_DUMP));
        if (!locker.try_lock()) {
            INFO() << "Database is already locked. Task interrupted.";
            return EXIT_SUCCESS;
        }

        INFO() << "Start dumping outsource regions";

        auto messages = createMessages(locker.writableTxn());
        writeMessages(messages);

        INFO() << "Finished dumping outsource regions";
        return EXIT_SUCCESS;
    } catch (const maps::Exception& ex) {
        FATAL() << "Worker failed: " << ex;
        return EXIT_FAILURE;
    } catch (const std::exception& ex) {
        FATAL() << "Worker failed: " << ex.what();
        return EXIT_FAILURE;
    }
}
