#include "tool.h"

#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/pgpool/include/pgpool3.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/common.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/track_point.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/track_point_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/graph_matcher_adapter/include/feature_positioner.h>
#include <maps/libs/common/include/make_batches.h>
#include <maps/libs/geolib/include/distance.h>

#include <boost/accumulators/accumulators.hpp>
#include <boost/accumulators/statistics.hpp>

#include <fstream>
#include <sstream>

namespace maps::mrc {
namespace {

struct FeatureManipulator {
    const db::Feature& feature;

    std::string url() const
    {
        std::ostringstream result;
        result << "https://core-nmaps-mrc-browser.maps.yandex.ru/feature/"
               << feature.id() << "/image";
        return result.str();
    }

    void json(json::ObjectBuilder b) const
    {
        b["type"] = "Feature";
        b["id"] = feature.id();
        b["geometry"] << geolib3::geojson(feature.geodeticPos());
        b["properties"] = [&](json::ObjectBuilder b) {
            b["image"] = url();
            b["heading"] = (int)feature.heading().value();
            b["source_id"] = feature.sourceId();
            b["date"] = chrono::formatIsoDateTime(feature.timestamp());
        };
    }
};

void updateFeatures(pgpool3::Pool& pool, db::Features& features)
{
    INFO() << "updating features";
    auto txn = pool.masterWriteableTransaction();
    auto batches = maps::common::makeBatches(features, 100);
    size_t counter = 0;
    for (auto& batch : batches) {
        auto arrayRef = TArrayRef<db::Feature>{
            const_cast<db::Features::iterator>(batch.begin()),
            const_cast<db::Features::iterator>(batch.end())};
        db::FeatureGateway(*txn).update(arrayRef, db::UpdateFeatureTxn::Yes);
        counter += std::distance(batch.begin(), batch.end());
        INFO() << "updated " << counter << " features";
    }
    txn->commit();
    INFO() << "committed";
}

void positioning(db::GraphType graphType,
                 const std::string& graph,
                 const db::TrackPoints& trackPoints,
                 db::Features& features)
{
    adapters::CompactGraphMatcherAdapter matcher{graph};
    auto trackPointProvider = [&](auto&&...) { return trackPoints; };
    adapters::FeaturePositioner positioner{{{graphType, &matcher}},
                                           trackPointProvider};
    positioner(features);
}

auto lessTime = [](auto& l, auto& r) { return l.timestamp() < r.timestamp(); };

double medianDistance(const db::TrackPoints& sortedTrackPoints,
                      const db::Features& features)
{
    boost::accumulators::accumulator_set<
        double,
        boost::accumulators::features<boost::accumulators::tag::median> >
        acc;
    for (const auto& feature : features) {
        auto it =
            std::lower_bound(sortedTrackPoints.begin(),
                             sortedTrackPoints.end(),
                             db::TrackPoint{}.setTimestamp(feature.timestamp()),
                             lessTime);
        auto dist = std::min(
            it == sortedTrackPoints.begin()
                ? std::numeric_limits<double>::max()
                : fastGeoDistance(feature.geodeticPos(),
                                  std::prev(it)->geodeticPos()),
            it == sortedTrackPoints.end()
                ? std::numeric_limits<double>::max()
                : fastGeoDistance(feature.geodeticPos(), it->geodeticPos()));
        acc(dist);
    }
    return boost::accumulators::median(acc);
}

void toJson(const db::Features& features, const std::string& outFile)
{
    std::ofstream os(outFile);
    json::Builder builder(os);
    builder << [&](json::ObjectBuilder b) {
        b["type"] = "FeatureCollection";
        b["features"] << [&](json::ArrayBuilder b) {
            for (const auto& feature : features) {
                b << FeatureManipulator{feature};
            }
        };
    };
}

}  // namespace

void run(common::Config& cfg,
         const std::string& sourceId,
         chrono::TimePoint startTime,
         chrono::TimePoint endTime,
         const std::string& pedestrianGraph,
         const std::string& roadGraph,
         const std::string& pedestrianJson,
         const std::string& roadJson,
         bool dryRun)
{
    using namespace std::chrono_literals;
    auto postgres = cfg.makePoolHolder();

    INFO() << "source_id " << sourceId << " between "
           << maps::chrono ::formatIsoDateTime(startTime) << " and "
           << maps::chrono ::formatIsoDateTime(endTime);

    const auto features =
        db::FeatureGateway(*postgres.pool().slaveTransaction())
            .load(db::table::Feature::sourceId == sourceId &&
                  db::table::Feature::date.between(startTime, endTime));
    INFO() << "loaded " << features.size() << " features";

    const auto trackPoints =
        db::TrackPointGateway(*postgres.pool().slaveTransaction())
            .load(db::table::TrackPoint::sourceId == sourceId &&
                      db::table::TrackPoint::timestamp.between(startTime - 1min,
                                                               endTime + 1min),
                  orderBy(db::table::TrackPoint::timestamp));
    INFO() << "loaded " << trackPoints.size() << " track points";

    auto pedestrianFeatures = features;
    positioning(db::GraphType::Pedestrian,
                pedestrianGraph,
                trackPoints,
                pedestrianFeatures);
    auto pedestrianMedian = medianDistance(trackPoints, pedestrianFeatures);
    INFO() << "median of " << pedestrianGraph << ": " << pedestrianMedian;
    if (!pedestrianJson.empty()) {
        toJson(pedestrianFeatures, pedestrianJson);
    }

    auto roadFeatures = std::move(features);
    positioning(db::GraphType::Road, roadGraph, trackPoints, roadFeatures);
    auto roadMedian = medianDistance(trackPoints, roadFeatures);
    INFO() << "median of " << roadGraph << ": " << roadMedian;
    if (!roadJson.empty()) {
        toJson(roadFeatures, roadJson);
    }

    if (!dryRun) {
        constexpr double ROAD_FACTOR = 0.75;
        if (pedestrianMedian < (ROAD_FACTOR * roadMedian)) {
            updateFeatures(postgres.pool(), pedestrianFeatures);
        }
        else {
            updateFeatures(postgres.pool(), roadFeatures);
        }
    }
}

}  // namespace maps::mrc
