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

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/common.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/frame_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/recognition_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object_gateway.h>

#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/features_reader.h>

#include <maps/libs/common/include/exception.h>
#include <maps/libs/common/include/make_batches.h>

#include <maps/libs/geolib/include/contains.h>
#include <maps/libs/geolib/include/spatial_relation.h>

#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/sql_chemistry/include/batch_load.h>

#include <maps/wikimap/mapspro/services/mrc/browser/lib/graph.h>

using namespace maps;
using namespace maps::mrc;

namespace {

db::TIds loadFeatureIds(
    const std::string& datasetDirectory,
    const std::string& graphDirectory,
    double minLon, double minLat,
    double maxLon, double maxLat)
{
    auto features = fb::FeaturesReader(datasetDirectory);
    auto graph = browser::Graph(graphDirectory, EMappingMode::Standard);

    const geolib3::BoundingBox geoBbox{
        geolib3::Point2(minLon, minLat),
        geolib3::Point2(maxLon, maxLat)
    };

    db::TIds featureIds = graph.getFeatureIdsByBbox(geoBbox);

    featureIds.erase(
        std::remove_if(featureIds.begin(), featureIds.end(),
            [&](const auto& id) {
                auto feature = features.featureById(id);

                if (!feature.has_value()) {
                    return true;
                }

                if (!feature->hasCameraDeviation()
                    || feature->cameraDeviation() != db::CameraDeviation::Front)
                {
                    return true;
                }

                return false;
            }
        ),
        featureIds.end()
    );

    return featureIds;
}

db::TIdSet loadFrameIds(
    maps::wiki::common::PoolHolder& pool,
    const db::TIds& featureIds)
{
    static const size_t BATCH_SIZE = 10000;

    auto txn = pool.pool().slaveTransaction();

    db::TIds frameIds;
    for (const auto& batch : maps::common::makeBatches(featureIds, BATCH_SIZE)) {
        auto items = db::eye::FeatureToFrameGateway{*txn}.load(
            db::eye::table::FeatureToFrame::featureId.in({batch.begin(), batch.end()})
        );

        for (const auto& item : items) {
            frameIds.push_back(item.frameId());
        }
    }

    return {frameIds.begin(), frameIds.end()};
}

db::TIdSet loadFrameIds(const std::string& path) {
    std::ifstream ifs(path);
    REQUIRE(ifs.is_open(), "Failed to open file: " << path);

    db::TIdSet frameIds;
    while(!ifs.eof()) {
        std::string line;
        std::getline(ifs, line);

        if (line.empty()) {
            continue;
        }

        db::TId frameId = std::stoull(line);
        frameIds.insert(frameId);
    }

    return frameIds;
}

void upTxnId(
    maps::wiki::common::PoolHolder& pool,
    const db::TIdSet& frameIds,
    size_t batchSize,
    size_t timeout,
    std::ofstream& ofs)
{
    for (const auto& batch : maps::common::makeBatches(frameIds, batchSize)) {
        auto txn = pool.pool().masterWriteableTransaction();

        auto frames = db::eye::FrameGateway{*txn}.loadByIds({batch.begin(), batch.end()});
        db::eye::FrameGateway{*txn}.upsertx(frames);

        txn->commit();

        for (const auto& frame : frames) {
            ofs << frame.id() << std::endl;
        }

        std::this_thread::sleep_for(std::chrono::minutes(timeout));
    }
}

} // namespace

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

    auto secretVersion = parser.string("secret-version")
            .help("version for secrets from yav.yandex-team.ru");

    auto mrcConfigPath = parser.string("mrc-config")
            .required()
            .help("path to mrc config");

    auto datasetDirectory = parser.string("dataset")
            .required()
            .help("path to dataset");

    auto graphDirectory = parser.string("graph")
            .required()
            .help("path to graph");

    auto minLat = parser.real("min_lat")
            .required()
            .help("Minimum latitude value of bbox");

    auto maxLat = parser.real("max_lat")
            .required()
            .help("Maximum latitude value of bbox");

    auto minLon = parser.real("min_lon")
            .required()
            .help("Minimum longitude value of bbox");

    auto maxLon = parser.real("max_lon")
            .required()
            .help("Maximum longitude value of bbox");

    auto updatedFramesPath = parser.string("updated_frames")
            .required()
            .help("Path to txt file with updated frame ids");

    auto excludeFramesPath = parser.string("exclude_frames")
            .help("Path to txt filewith frame ids to exclude");

    auto timeout = parser.num("timeout")
            .help("wait for db update in minutes (default 5)")
            .defaultValue(10);

    auto batchSize = parser.num("batch")
            .defaultValue(10000)
            .help("frame batch size (default 10000)");

    parser.parse(argc, const_cast<char**>(argv));

    const auto config = mrc::common::templateConfigFromCmdPath(secretVersion, mrcConfigPath);
    auto pool = config.makePoolHolder();

    INFO() << "Loading features";
    db::TIds featureIds = loadFeatureIds(
        datasetDirectory, graphDirectory,
        minLon, minLat, maxLon, maxLat
    );
    INFO() << featureIds.size() << " features loaded";

    INFO() << "Loading frames";
    db::TIdSet frameIds = loadFrameIds(pool, featureIds);
    INFO() << frameIds.size() << " frames loaded";

    INFO() << "Excluding frames";
    db::TIdSet excludeFrameIds;
    if (excludeFramesPath.defined()) {
        excludeFrameIds = loadFrameIds(excludeFramesPath);
    }

    db::TIdSet finalFrameIds;
    std::set_difference(
        frameIds.begin(), frameIds.end(),
        excludeFrameIds.begin(), excludeFrameIds.end(),
        std::inserter(finalFrameIds, finalFrameIds.end()));
    INFO() << finalFrameIds.size() << " frames left";

    INFO() << "Up txn id";
    std::ofstream ofs(updatedFramesPath);
    REQUIRE(ofs.is_open(), "Failed to open file: " << updatedFramesPath);
    for (db::TId frameId : excludeFrameIds) {
        ofs << frameId << std::endl;
    }

    upTxnId(pool, finalFrameIds, batchSize, timeout, ofs);

    ofs.close();
    INFO() << "Done";

    return EXIT_SUCCESS;
} catch (const maps::Exception& ex) {
    FATAL() << "Failed: " << ex;
    return EXIT_FAILURE;
} catch (const std::exception& ex) {
    FATAL() << "Failed: " << ex.what();
    return EXIT_FAILURE;
} catch (...) {
    FATAL() << "Unknown error!";
}
