#include <yandex/maps/wiki/validator/storage/exclusions_gateway.h>
#include <yandex/maps/wiki/revision/common.h>
#include <yandex/maps/wiki/revision/filters.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <yandex/maps/wiki/revision/branch_manager.h>
#include <yandex/maps/wiki/common/pgpool3_helpers.h>
#include <yandex/maps/wiki/common/extended_xml_doc.h>
#include <yandex/maps/wiki/common/default_config.h>
#include <yandex/maps/wiki/common/geom.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/pgpool/include/pgpool3.h>
#include <maps/libs/log8/include/log8.h>

#include <geos/io/WKBReader.h>
#include <geos/geom/Polygon.h>

#include <boost/program_options/variables_map.hpp>
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/parsers.hpp>

#include <sstream>
#include <vector>

namespace pgpool = maps::pgpool3;
namespace po = boost::program_options;
namespace rev = maps::wiki::revision;
namespace rf = rev::filters;
namespace common = maps::wiki::common;
namespace vs = maps::wiki::validator::storage;

using PolygonVec = std::vector<common::Geom>;

namespace {

const std::string OPT_HELP = "help";
const std::string OPT_CONFIG = "config";
const std::string OPT_LOG_LEVEL = "log-level";
const std::string OPT_DRY_RUN = "dry-run";

const std::string CORE_DATABASE_NAME = "long-read";
const std::string CORE_POOL_NAME = "long-read";
const std::string VALIDATION_DATABASE_NAME = "validation";
const std::string VALIDATION_POOL_NAME = "editor";

const std::string CAT_DIFFALERT_REGION = "cat:diffalert_region";

constexpr size_t BATCH_SIZE = 100;

} // namespace

namespace maps {
namespace wiki {

PolygonVec loadImportantRegions(const rev::Snapshot& snapshot)
{
    INFO() << "Start loading important regions";

    auto revisionFilter =
        rf::Attr(CAT_DIFFALERT_REGION).defined()
        && rf::ObjRevAttr::isNotRelation()
        && rf::ObjRevAttr::isNotDeleted()
        && rf::Geom::defined();

    PolygonVec importantRegions;
    for (const auto& rev : snapshot.objectRevisionsByFilter(revisionFilter)) {
        REQUIRE(rev.data().geometry, "Object " << rev.id() << " has no geometry");
        common::Geom geom(*rev.data().geometry);
        REQUIRE(geom->getGeometryTypeId() == geos::geom::GEOS_POLYGON,
            "Failed to cast object " << rev.id() << " geometry to polygon");
        importantRegions.push_back(std::move(geom));
    }
    return importantRegions;
}

void updateExclusion(
    const vs::StoredMessageDatum& datum,
    validator::RegionType newRegionType,
    pgpool3::TransactionHandle& txn)
{
    auto contentId = datum.id().contentId();
    auto oldAttributesId = datum.id().attributesId();

    INFO() << "Try replace important_region for exclusion "
        << oldAttributesId << "-" << contentId;

    std::stringstream selectAttributesIdQuery;
    selectAttributesIdQuery
        << "SELECT validation.insert_message_attributes("
        << static_cast<unsigned int>(datum.message().attributes().severity) << ", "
        << txn->quote(datum.message().attributes().checkId) << ", "
        << txn->quote(datum.message().attributes().description) << ", "
        << (newRegionType == validator::RegionType::Important ? "TRUE" : "FALSE") << ")";
    auto newAttributesResult = txn->exec(selectAttributesIdQuery.str());
    REQUIRE(!newAttributesResult.empty(), "Failed to compute new attributes id");
    auto newAttributesId = newAttributesResult[0][0].as<uint32_t>();

    std::stringstream selectExistingExclusionQuery;
    selectExistingExclusionQuery
        << "SELECT 1 FROM validation.exclusion "
        << "WHERE attributes_id = " << newAttributesId << " "
        << "AND content_id = " << contentId;
    auto existingExclusionResult = txn->exec(selectExistingExclusionQuery.str());

    if (existingExclusionResult.empty()) {
        INFO() << "Update exclusion "
            << oldAttributesId << "-" << contentId
            << " to new attributes " << newAttributesId;

        std::stringstream query;
        query
            << "UPDATE validation.exclusion "
            << "SET attributes_id = " << newAttributesId << " "
            << "WHERE attributes_id = " << oldAttributesId << " "
            << "AND content_id = " << contentId;
        txn->exec(query.str());
    } else {
        WARN() << "Delete old exclusion "
            << oldAttributesId << "-" << contentId;

        std::stringstream query;
        query
            << "DELETE FROM validation.exclusion "
            << "WHERE attributes_id = " << oldAttributesId << " "
            << "AND content_id = " << contentId;
        txn->exec(query.str());
    }
}

void processBatch(vs::StoredMessageData data, const PolygonVec& importantRegions, pgpool3::TransactionHandle& txn)
{
    for (const auto& datum : data) {
        auto geomWkb = datum.message().geomWkb();
        if (geomWkb.empty()) {
            continue;
        }

        common::Geom geom(geomWkb);

        auto regionType = datum.message().attributes().regionType;
        auto newRegionType = validator::RegionType::Unimportant;
        for (const auto& region : importantRegions) {
            if (region->intersects(geom.geosGeometryPtr())) {
                newRegionType = validator::RegionType::Important;
                break;
            }
        }
        if (newRegionType != regionType) {
            updateExclusion(datum, newRegionType, txn);
        }
    }
}

void run(const common::ExtendedXmlDoc& configXml, bool dryRun)
{
    common::PoolHolder coreDbHolder(configXml, CORE_DATABASE_NAME, CORE_POOL_NAME);
    common::PoolHolder validationDbHolder(configXml, VALIDATION_DATABASE_NAME, VALIDATION_POOL_NAME);

    auto txnCore = coreDbHolder.pool().slaveTransaction();
    auto txnValidation = validationDbHolder.pool().masterWriteableTransaction();

    rev::RevisionsGateway gateway(*txnCore);
    auto commitId = gateway.headCommitId();
    auto snapshot = gateway.stableSnapshot(commitId);

    auto importantRegions = loadImportantRegions(snapshot);

    vs::ExclusionsFilter statsFilter;
    statsFilter.createdBefore = std::chrono::system_clock::now();

    vs::ExclusionsGateway exclusionsGateway(*txnValidation);
    auto statistics = exclusionsGateway.statistics(statsFilter);

    size_t totalCount = 0;
    for (const auto& stat : statistics) {
        totalCount += stat.second;
    }

    INFO() << "Exclusions total count " << totalCount;

    size_t NBatches = totalCount / BATCH_SIZE + 1;
    for (size_t i = 0; i < NBatches; i++) {
        INFO() << "BATCH " << i << "/" << NBatches;
        processBatch(
            exclusionsGateway.exclusions(statsFilter, snapshot, i * BATCH_SIZE, BATCH_SIZE),
            importantRegions,
            txnValidation);
    }

    if (!dryRun) {
        txnValidation->commit();
    }
}

} // namespace wiki
} // namespace maps

/**
Utility to recompute important_region attribute of validation exclusions
*/
int main(int argc, char* argv[])
{
    po::options_description desc("Allowed options");
    desc.add_options()
        (OPT_HELP.c_str(), "produce help message")
        (OPT_CONFIG.c_str(), po::value<std::string>(), "config path")
        (OPT_LOG_LEVEL.c_str(), po::value<std::string>(), "log level")
        (OPT_DRY_RUN.c_str(), "simulate operation (without commit)");

    po::variables_map vm;
    po::store(po::parse_command_line(argc, argv, desc), vm);
    po::notify(vm);

    if (vm.count(OPT_HELP)) {
        std::cout << desc << std::endl;
        return 1;
    }

    if (vm.count(OPT_LOG_LEVEL)) {
        maps::log8::setLevel(maps::log8::strToLevel(vm[OPT_LOG_LEVEL].as<std::string>()));
    }

    std::unique_ptr<common::ExtendedXmlDoc> configDocPtr;
    if (vm.count(OPT_CONFIG)) {
        configDocPtr.reset(new common::ExtendedXmlDoc(vm[OPT_CONFIG].as<std::string>()));
    } else {
        configDocPtr = common::loadDefaultConfig();
    }

    auto dryRun = vm.count(OPT_DRY_RUN) > 0;

    try {
        maps::wiki::run(*configDocPtr, dryRun);
        return 0;
    } catch (const maps::Exception& e) {
        std::cerr << e << std::endl;
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
    return 1;
}
