#include "tool.h"

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>

#include <maps/libs/json/include/value.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/for_each_batch.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/visibility.h>

#include <geos/geom/CoordinateArraySequence.h>
#include <geos/geom/Envelope.h>
#include <geos/geom/GeometryFactory.h>
#include <geos/geom/Point.h>
#include <geos/geom/Polygon.h>
#include <geos/io/WKBReader.h>
#include <pqxx/binarystring>
#include <pqxx/connection>
#include <pqxx/nontransaction>

#include <sstream>

namespace maps {
namespace mrc {
namespace toloka {

namespace {

constexpr size_t BATCH_SIZE = 4000;

const std::string FIELD_CONNSTR = "connstr";
const std::string FIELD_DESCRIPTION = "description";
const std::string FIELD_GARDEN = "garden";
const std::string FIELD_QUERY = "query";
const std::string FIELD_SHAPES = "shapes";

const geos::geom::GeometryFactory* const geosFactory
    = geos::geom::GeometryFactory::getDefaultInstance();

GeomPtr boxToGeosGeometry(const geolib3::BoundingBox& bbox)
{
    geos::geom::Envelope env(bbox.minX(), bbox.maxX(),
                             bbox.minY(), bbox.maxY());
    return GeomPtr{geosFactory->toGeometry(&env)};
}

} // anonymous namespace

Strings loadYmapsdfGeometries(const std::string& toolConfigPath)
{
    auto cfg = json::Value::fromFile(toolConfigPath);
    auto gardenCfg = cfg[FIELD_GARDEN];

    pqxx::connection conn(gardenCfg[FIELD_CONNSTR].as<std::string>());
    pqxx::nontransaction txn(conn);

    Strings geoms;
    for (const auto& item : gardenCfg[FIELD_SHAPES]) {
        INFO() << "Loading: " << item[FIELD_DESCRIPTION].as<std::string>();
        auto rows = txn.exec(item[FIELD_QUERY].as<std::string>());
        INFO() << "Loaded: " << rows.size();

        for (const auto& row : rows) {
            const auto& field = row.front();
            if (!field.is_null()) {
                geoms.push_back(pqxx::binarystring(field).str());
            }
        }
    }
    return geoms;
}

RTree makeRTree(const Strings& wkbGeoms)
{
    INFO() << "Start building R-tree";
    RTree rtree;

    for (auto& wkb : wkbGeoms) {
        std::istringstream iss(wkb);
        GeomPtr geomPtr = geos::io::WKBReader().read(iss);
        geomPtr->normalize();
        const geos::geom::Envelope* envelope = geomPtr->getEnvelopeInternal();

        Box box(Point(envelope->getMinX(), envelope->getMinY()),
                Point(envelope->getMaxX(), envelope->getMaxY()));

        rtree.insert({box, geomPtr});
    }
    INFO() << "Finished building R-tree";
    return rtree;
}

db::TIds loadAffectedFeatureIds(pqxx::transaction_base& txn,
                                const RTree& rTree)
{
    db::FeatureGateway gtw(txn);
    auto ids = gtw.loadIds(db::table::Feature::isPublished);
    INFO() << "Total features: " << ids.size();

    db::TIds featureIds;

    common::forEachBatch(ids, BATCH_SIZE, [&](db::TIds::iterator begin,
                                              db::TIds::iterator end) {
        auto features = gtw.loadByIds(db::TIds{begin, end});

        for (const auto& feature : features) {
            auto box = db::addVisibilityMargins(feature.geodeticPos().boundingBox());
            auto boostBox
                = Box{{box.minX(), box.minY()}, {box.maxX(), box.maxY()}};
            auto geosBox = boxToGeosGeometry(box);

            BoxAndGeomPairs pairs;
            rTree.query(boost::geometry::index::intersects(boostBox),
                        std::back_inserter(pairs));

            for (const auto& pair : pairs) {
                if (pair.second->intersects(geosBox.get())) {
                    featureIds.push_back(feature.id());
                    break;
                }
            }
        }
    });

    INFO() << "Selected features: " << featureIds.size();
    return featureIds;
}

} // toloka
} // mrc
} // maps
