#include "convert.h"
#include "exporter.h"

#include <maps/libs/common/include/make_batches.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/retry.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/coverage_rtree_writer.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/features_writer.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/photo_to_edge_pairs_writer.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/walk_objects_writer.h>
#include <maps/wikimap/mapspro/services/mrc/libs/fb/include/write.h>

#include <boost/range/algorithm_ext/erase.hpp>

#include <filesystem>

namespace fs = std::filesystem;

namespace maps::mrc::export_gen {
namespace {

constexpr size_t BATCH_SIZE = 4000;

FeaturesSummary generateFbFeatures(const ILoader& loader,
                                   const std::string& mrcDatasetDir,
                                   const std::string& featuresSecretDatasetDir,
                                   const std::string& version,
                                   db::TId lastFeatureTxnId,
                                   fb::TSchemaVersion schemaVersion)
{
    size_t featuresNumber = 0;
    size_t batchesNumber = 0;
    auto result = FeaturesSummary{.fbVersion = std::string{loader.fbVersion()}};
    auto secretFeaturesWriter = fb::FeaturesWriter{};
    auto defaultFeaturesWriter = fb::FeaturesWriter{};

    INFO() << "loading features";
    loader.forEachPublishedFeatures(
        BATCH_SIZE,
        [&](db::Features&& features, ObjectsInPhotoMap&& objectsInPhoto, Origin origin) {
            for (auto& feature : features) {
                /// @see https://st.yandex-team.ru/MAPSMRC-3144
                if (feature.graph() == db::GraphType::Pedestrian &&
                    feature.cameraDeviation() != db::CameraDeviation::Front) {
                    feature.setCameraDeviation(db::CameraDeviation::Front);
                }
                result.photos.push_back(toPhoto(feature, origin));
                db::ObjectsInPhoto objects;
                auto it = objectsInPhoto.find(feature.id());
                if (it != objectsInPhoto.end()) {
                    objects = it->second;
                }
                if (feature.privacy() == db::FeaturePrivacy::Secret) {
                    secretFeaturesWriter.add(feature, objects);
                } else {
                    defaultFeaturesWriter.add(feature, objects);
                }
                if (!feature.isPublished()) {
                    result.newToPublish.push_back(feature.id());
                }
            }
            featuresNumber += features.size();
            if (0 == ++batchesNumber % 100) {
                INFO() << "loaded " << format(featuresNumber) << " features";
            }
        });
    INFO() << "done loading features " << format(featuresNumber);

    INFO() << "saving features";
    defaultFeaturesWriter.dump(
        version, lastFeatureTxnId, schemaVersion, mrcDatasetDir);
    secretFeaturesWriter.dump(
        version, lastFeatureTxnId, schemaVersion, featuresSecretDatasetDir);
    INFO() << "done saving features";

    return result;
}

void generateFbWalkObjects(const ILoader& loader,
                           const std::string& destDir,
                           const std::string& version)
{
    size_t walkObjectsNumber = 0;
    size_t batchesNumber = 0;
    auto walkObjectsWriter = fb::WalkObjectsWriter{};

    INFO() << "loading walk objects";
    loader.forEachWalkObjects(
        BATCH_SIZE,
        [&](db::WalkObjects&& walkObjects) {
            for (const db::WalkObject& walkObject : walkObjects) {
                /// frontend and browser only work with point objects
                if (walkObject.geometryType() == geolib3::GeometryType::Point) {
                    walkObjectsWriter.add(walkObject);
                    ++walkObjectsNumber;
                }
            }
            if (0 == ++batchesNumber % 100) {
                INFO() << "loaded " << format(walkObjectsNumber)
                       << " walk objects";
            }
        });
    INFO() << "done loading walk objects " << format(walkObjectsNumber);

    INFO() << "saving walk objects";
    walkObjectsWriter.dump(version, destDir);
    INFO() << "done saving walk objects";
}

void prepareExportDirectory(const std::string& path)
{
    int deletedNumber = fs::remove_all(path);
    if (deletedNumber > 0) {
        INFO()  << "Deleted " << deletedNumber << " files from '" << path << "'";
    }

    fs::create_directories(path);
}

db::TIdSet collectSecretFeatureIds(const Photos& photos)
{
    auto result = db::TIdSet{};
    for (const auto& photo : photos) {
        if (photo.privacy == db::FeaturePrivacy::Secret) {
            result.insert(photo.featureId);
        }
    }
    return result;
}

} //namespace

FeaturesSummary generateExport(const ILoader& loader,
                               const std::string& mrcDatasetDir,
                               const std::string& featuresSecretDatasetDir,
                               const std::string& version,
                               db::TId lastFeatureTxnId,
                               fb::TSchemaVersion featureSchemaVersion)
{
    prepareExportDirectory(mrcDatasetDir);
    prepareExportDirectory(featuresSecretDatasetDir);
    auto result = generateFbFeatures(loader,
                                     mrcDatasetDir,
                                     featuresSecretDatasetDir,
                                     version,
                                     lastFeatureTxnId,
                                     featureSchemaVersion);
    generateFbWalkObjects(loader, mrcDatasetDir, version);
    return result;
}

void generateGraph(const GraphDescriptor& graphDescriptor,
                   pgpool3::Pool& pool,
                   const Photos& photos,
                   const std::string& destDir,
                   const std::string& destDirPro,
                   const std::string& destDirPhotoToEdge,
                   const std::string& destDirPhotoToEdgePro,
                   const std::string& mrcVersion,
                   const std::string& prevMrcVersion)
{
    static const auto files =
        std::vector{EDGES_PERSISTENT_INDEX_FILE, ROAD_GRAPH_FILE, RTREE_FILE};
    auto ctx = common::retryOnException<maps::Exception>(
        common::RetryPolicy()
            .setInitialTimeout(std::chrono::seconds(1))
            .setMaxAttempts(3)
            .setTimeoutBackoff(2),
        [&] {
            for (const auto& path : {destDir,
                                     destDirPro,
                                     destDirPhotoToEdge,
                                     destDirPhotoToEdgePro}) {
                prepareExportDirectory(path);
            }
            auto canonicals = std::vector<fs::path>{};
            for (const auto& file : files) {
                canonicals.push_back(
                    fs::canonical(graphDescriptor.graphDir + "/" + file));
            }
            for (size_t i = 0; i < files.size(); ++i) {
                const auto& file = files[i];
                const auto& canonical = canonicals[i];
                fs::copy_file(canonical, destDir + "/" + file);
                fs::create_symlink(destDir + "/" + file,
                                   destDirPro + "/" + file);
            }
            return Context{destDir,
                           graphDescriptor.graphType,
                           prevMrcVersion,
                           graphDescriptor.prevGraphDir,
                           graphDescriptor.prevPhotoToEdgeDir};
        });
    auto [graph, photoToEdgePairs] =
        makeGraphSummary(ctx, photos, makeTrackPointProvider(pool));
    graph.mrcVersion = mrcVersion;
    graph.schemaVersion = CURRENT_GRAPH_SCHEMA_VERSION;
    fb::writeToFile(graph, destDirPro + "/" + GRAPH_COVERAGE_FILE);
    fb::writeCoverageRtreeToDir(ctx.matcher.graph(), graph, destDirPro);
    fb::writePhotoToEdgePairsToFile(
        graph.version,
        mrcVersion,
        CURRENT_PHOTO_TO_EDGE_SCHEMA_VERSION,
        photoToEdgePairs,
        destDirPhotoToEdgePro + "/" + PHOTO_TO_EDGE_FILE);

    for (auto& edge : graph.edges) {
        boost::remove_erase_if(edge.coverages, [](const auto& coverage) {
            return coverage.privacy == db::FeaturePrivacy::Secret;
        });
    }
    boost::remove_erase_if(
        graph.edges, [](const auto& edge) { return edge.coverages.empty(); });
    fb::writeToFile(graph, destDir + "/" + GRAPH_COVERAGE_FILE);
    fb::writeCoverageRtreeToDir(ctx.matcher.graph(), graph, destDir);

    auto secretFeatureIds = collectSecretFeatureIds(photos);
    boost::remove_erase_if(photoToEdgePairs, [&](const auto& photoToEdgePair) {
        return secretFeatureIds.contains(photoToEdgePair.featureId());
    });
    fb::writePhotoToEdgePairsToFile(
        graph.version,
        mrcVersion,
        CURRENT_PHOTO_TO_EDGE_SCHEMA_VERSION,
        photoToEdgePairs,
        destDirPhotoToEdge + "/" + PHOTO_TO_EDGE_FILE);
}

void generateActivation(const std::string& destDir, const std::string& version)
{
    auto deletedNumber = fs::remove_all(destDir);
    INFO() << destDir << ": deleted " << deletedNumber << " files";
    fs::create_directories(destDir);
    auto os = std::ofstream{destDir + "/version"};
    os << version;
}

} //namespace maps::mrc::export_gen
