#include <maps/wikimap/mapspro/services/mrc/eye/lib/import_yt_panorama/include/constants.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/import_yt_panorama/include/yt_panorama_importer.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/pg_locks.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye_panorama_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/metadata_gateway.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>

#include <iterator>
#include <limits>
#include <unordered_map>

namespace maps::mrc::eye {
namespace {

bool isPanoRevisionChanged(
    maps::pgpool3::Pool& pool, const std::uint64_t& panoramaRevision)
{
    auto txn = pool.masterReadOnlyTransaction();

    const auto descriptionRevision =
        maps::mrc::db::MetadataGateway{*txn}.tryLoadByKey<std::uint64_t>(
            DB_YT_PANORAMA_REVISION_METADATA_KEY, 0u);

    return !descriptionRevision || panoramaRevision != descriptionRevision;
}

bool isPrecedingInSession(const PanoramaDescription lhs,
                          const PanoramaDescription rhs)
{
    return std::tie(lhs.sessionId, lhs.orderNum) <
           std::tie(rhs.sessionId, rhs.orderNum);
};

std::vector<PanoramaDescriptions> makePanoramaSessions(
    PanoramaDescriptions panoramaDescriptions)
{
    std::vector<PanoramaDescriptions> sessions;

    if (panoramaDescriptions.empty()) {
        return sessions;
    }

    // Sort lexicographically by session and order numbers
    std::sort(
        panoramaDescriptions.begin(),
        panoramaDescriptions.end(),
        isPrecedingInSession);

    const auto addSession =
        [&](PanoramaDescriptions::const_iterator sessionBegin,
            PanoramaDescriptions::const_iterator sessionEnd) {
            sessions.emplace_back(sessionBegin, sessionEnd);
        };

    PanoramaDescriptions::const_iterator sessionBegin =
        panoramaDescriptions.cbegin();
    PanoramaDescriptions::const_iterator sessionEnd =
        std::next(panoramaDescriptions.cbegin());

    for (; sessionEnd != panoramaDescriptions.cend(); ++sessionEnd) {
        if (sessionBegin->sessionId != sessionEnd->sessionId) {
            addSession(sessionBegin, sessionEnd);
            sessionBegin = sessionEnd;
        }
    }
    addSession(sessionBegin, sessionEnd);

    const auto median =
        [](const auto& session) -> const PanoramaDescription& {
        return *std::next(session.cbegin(), session.size() / 2);
    };

    std::sort(
        sessions.begin(),
        sessions.end(),
        [median](const auto& lhs, const auto& rhs) {
            return median(lhs).date < median(rhs).date;
        });

    return sessions;
}

std::vector<std::string> getSortedOIDs(
    const PanoramaDescriptions& panoramaDescriptions)
{
    std::vector<std::string> sortedOIDs;
    sortedOIDs.reserve(panoramaDescriptions.size());
    for (const auto& panoramaDescription: panoramaDescriptions) {
        sortedOIDs.push_back(panoramaDescription.oid);
    }
    std::sort(sortedOIDs.begin(), sortedOIDs.end());

    return sortedOIDs;
}

db::EyePanoramas createDbPanoramas(
    const PanoramaDescriptions& panoramaDescriptions)
{
    db::EyePanoramas result;

    for (const auto& panoramaDescription: panoramaDescriptions) {
        result.emplace_back(
            panoramaDescription.oid /* oid */,
            panoramaDescription.date /* date */,
            panoramaDescription.sessionId /* sessionId */,
            panoramaDescription.orderNum /* orderNum */,
            panoramaDescription.geodeticPos /* geodeticPos */,
            panoramaDescription.course /* vehicleCourse */,
            false /* deleted */);
    }

    return result;
}

void savePanoramasToDb(
    pgpool3::TransactionHandle& txn,
    const db::EyePanoramas& panoramas)
{
    // WARN: Panoramas are inserted session wise to avoid possible panorama
    //       IDs misordering between two different sessions. Misordering of
    //       panorama IDs within the same session don't have any negative
    //       effects.
    for (auto begin = panoramas.begin(); begin != panoramas.end();) {
        const auto end =
            std::find_if(begin, panoramas.end(), [&](const auto& panorama) {
                return panorama.sessionId() != begin->sessionId();
            });
        db::EyePanoramaGateway{*txn}.insertx(
            db::EyePanoramas{begin, end});
        begin = end;
    }
}

void updateDeletedPanoramas(
    pgpool3::TransactionHandle& txn,
    const std::vector<std::string>& deletedOIDs)
{
    auto deletedPanoramas = db::EyePanoramaGateway{*txn}.load(
        db::table::EyePanorama::oid.in(deletedOIDs) &&
        !db::table::EyePanorama::deleted);

    for (auto& deletedPanorama: deletedPanoramas) {
        deletedPanorama.setDeleted(true);
    }

    maps::mrc::db::EyePanoramaGateway{*txn}.updatex(deletedPanoramas);
}

} // anonymous namespace

YtPanoramaImporter::YtPanoramaImporter(
    const common::Config& mrcConfig, NYT::IClientPtr ytClient, bool commit)
    : mrcConfig_{mrcConfig}
    , pgPoolHolder_{mrcConfig_.makePoolHolder()}
    , pgMutex_{pgPoolHolder_.pool(), static_cast<int64_t>(common::LockId::EyeYtPanoramaImport)}
    , ytPanoramaLoader_{mrcConfig.externals().yt(), std::move(ytClient)}
    , commit_{commit}
{
    static std::once_flag onceFlag;
    std::call_once(onceFlag, [] { NYT::JoblessInitialize(); });
}

bool YtPanoramaImporter::lockDB()
{
    if (!pgMutex_.try_lock()) {
        return false;
    }
    return true;
}

bool YtPanoramaImporter::import(chrono::TimePoint updateTimePoint)
{
    INFO() << "Loading YT panorama dataset revision";
    const std::uint64_t panoramaRevision = ytPanoramaLoader_.loadRevision();

    INFO() << "Trying to import panoramas dataset revision "
           << panoramaRevision;

    if (!isPanoRevisionChanged(pgPoolHolder_.pool(), panoramaRevision)) {
        WARN()
            << "Panoramas revision didn't change since the last run, abort";
        updateMetadata(panoramaRevision, updateTimePoint);
        return false;
    }

    INFO() << "Load panoramas update from YT";
    auto [panoramaDescriptions, deletedOIDs] = getUpdate(
        ytPanoramaLoader().loadPanoramas());

    if (panoramaDescriptions.empty() && deletedOIDs.empty()) {
        INFO() << "Nothing to do this time for revision "
               << panoramaRevision;
        updateMetadata(panoramaRevision, updateTimePoint);
        return false;
    }

    if (!deletedOIDs.empty()) {
        INFO() << "Updating " << deletedOIDs.size() << " deleted panoramas";

        auto txn = pgPoolHolder_.pool().masterWriteableTransaction();
        updateDeletedPanoramas(txn, deletedOIDs);
        if (commit_) {
            INFO() << "Commit deleted panoramas";
            txn->commit();
        }
    }

    INFO() << "Make DB panoramas";
    // Stick together panoramas from within one shooting session and order
    // sessions by median panorama date. It is needed due to possibly
    // erroneous timestamps in early panoramas (according to @idg).
    const auto sessions =
        makePanoramaSessions(std::move(panoramaDescriptions));

    for (const auto& session: sessions) {
        INFO() << "Saving " << session.size() << " panorama from session "
               << session.at(0).sessionId << " to DB";

        const auto dbPanoramas = createDbPanoramas(session);

        auto txn = pgPoolHolder_.pool().masterWriteableTransaction();
        savePanoramasToDb(txn, dbPanoramas);
        if (commit_) {
            INFO() << "Commit panoramas from session " << session.at(0).sessionId;
            txn->commit();
        }
    }
    INFO() << "Done wih saving panoramas from revision " << panoramaRevision;

    updateMetadata(panoramaRevision, updateTimePoint);
    return true;
}

std::optional<YtPanoramaImporter::Metadata>
YtPanoramaImporter::loadMetadataFromDb(pgpool3::TransactionHandle& txn)
{
    db::MetadataGateway gtw{*txn};

    const auto descriptionRevision = gtw.tryLoadByKey<std::uint64_t>(
        DB_YT_PANORAMA_REVISION_METADATA_KEY, 0);

    const auto updatedAt =
        gtw.tryLoadByKey(DB_YT_PANORAMA_DATASET_UPDATED_AT_METADATA_KEY);

    if (!descriptionRevision || !updatedAt) {
        return std::nullopt;
    }

    return Metadata{chrono::parseSqlDateTime(*updatedAt), descriptionRevision};
}

YtPanoramaLoader& YtPanoramaImporter::ytPanoramaLoader()
{
    return ytPanoramaLoader_;
}

std::vector<std::string> YtPanoramaImporter::loadSortedExistingOIDs()
{
    auto existingOIDs =
        db::EyePanoramaGateway{
            *pgPoolHolder_.pool().masterReadOnlyTransaction()}
            .loadIds();

    std::sort(existingOIDs.begin(), existingOIDs.end());

    return existingOIDs;
}

YtPanoramaImporter::Update YtPanoramaImporter::getUpdate(
    PanoramaDescriptions panoramaDescriptions)
{
    const auto ytOIDs = getSortedOIDs(panoramaDescriptions);
    auto existingOIDs = loadSortedExistingOIDs();

    const auto panoramaDescriptionsEnd = std::remove_if(
        panoramaDescriptions.begin(),
        panoramaDescriptions.end(),
        [&](const auto& panoramaDescription) {
            return std::binary_search(
                existingOIDs.begin(),
                existingOIDs.end(),
                panoramaDescription.oid);
        });

    const auto deletedOIDsEnd = std::remove_if(
        existingOIDs.begin(),
        existingOIDs.end(),
        [&](const std::string& oid) {
            return std::binary_search(
                ytOIDs.begin(), ytOIDs.end(), oid);
        });

    return {
        {panoramaDescriptions.begin(), panoramaDescriptionsEnd},
        {existingOIDs.begin(), deletedOIDsEnd}};
}

void YtPanoramaImporter::updateMetadata(
    pgpool3::TransactionHandle& txn,
    std::uint64_t panoramaRevision,
    chrono::TimePoint updatedAt)
{
    db::MetadataGateway{*txn}.upsertByKey(
        DB_YT_PANORAMA_REVISION_METADATA_KEY,
        std::to_string(panoramaRevision));

    db::MetadataGateway{*txn}.upsertByKey(
        DB_YT_PANORAMA_DATASET_UPDATED_AT_METADATA_KEY,
        chrono::formatSqlDateTime(updatedAt));
}

void YtPanoramaImporter::updateMetadata(
    std::uint64_t panoramaRevision, chrono::TimePoint updatedAt)
{
    auto dbTxn = pgPoolHolder_.pool().masterWriteableTransaction();
    updateMetadata(dbTxn, panoramaRevision, updatedAt);

    if (commit_) {
        INFO() << "Commit metadata";
        dbTxn->commit();
    }
}

} // namespace maps::mrc::eye
