#include "constants.h"

#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/id.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/load.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/txn.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/util.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/import_panorama_frame/include/import.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/import_panorama_frame/include/metadata.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/location/include/move.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/location/include/rotation.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/frame_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/panorama_frame_gateway.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/units.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/log8/include/log8.h>

#include <library/cpp/iterator/zip.h>

#include <algorithm>
#include <set>

namespace maps::mrc::eye {

namespace {

using PanoramaDevices = std::map<geolib3::Degrees, db::eye::Device>;

using SesionIdToDevices = std::map<std::uint64_t, PanoramaDevices>;

template<typename T>
std::set<T> difference(std::vector<T> lhs, std::vector<T> rhs)
{
    std::sort(lhs.begin(), lhs.end());
    std::sort(rhs.begin(), rhs.end());
    std::vector<T> diff;
    std::set_difference(lhs.begin(), lhs.end(),
                        rhs.begin(), rhs.end(),
                        std::back_inserter(diff));
    return {diff.begin(), diff.end()};
}

template<typename T>
db::PanoramaOIDs collectOIDs(const std::vector<T>& entities)
{
    db::PanoramaOIDs oids;
    oids.reserve(entities.size());

    for (const auto& entity: entities) {
        oids.push_back(entity.oid());
    }

    return oids;
}

struct UpdateFramesResult {
    std::set<db::PanoramaOID> updatedOIDs;
    std::size_t updatedFramesCount;
};

UpdateFramesResult updateFrames(
    pqxx::transaction_base& txn, const db::EyePanoramas& panoramas)
{
    // WARN: Currently only deletion flag is updated.
    const auto oids = collectOIDs(panoramas);
    const auto panoramaToFrames = db::eye::PanoramaToFrameGateway{txn}.load(
        db::eye::table::PanoramaToFrame::oid.in(oids));

    // db::eye::PanoramaToFrame is one to many relation so collect multiple
    // frame IDs for a specific panorama OID.
    std::map<db::PanoramaOID, db::TIds> oidToFrameIds;
    for (const auto& panoramaToFrame: panoramaToFrames) {
        oidToFrameIds[panoramaToFrame.oid()].push_back(
            panoramaToFrame.frameId());
    }

    // Construct reverse mapping for each frame ID to its panorama.
    std::map<db::TId, const db::EyePanorama*> frameIdToPanorama;
    for (const auto& panorama: panoramas) {
        if (!oidToFrameIds.contains(panorama.oid())) {
            continue;
        }

        for (auto frameId: oidToFrameIds[panorama.oid()]) {
            frameIdToPanorama[frameId] = &panorama;
        }
    }

    // Finally load all frames and update their deleted flag
    auto frames =
        db::eye::FrameGateway{txn}.loadByIds(collectKeys(frameIdToPanorama));

    std::set<db::TId> unchangedFrameIds;

    for (auto& frame: frames) {
        const auto* panorama = frameIdToPanorama.at(frame.id());

        if (frame.deleted() != panorama->deleted()) {
            frame.setDeleted(panorama->deleted());
        } else {
            unchangedFrameIds.insert(frame.id());
        }
    }

    frames.erase(
        std::remove_if(
            frames.begin(),
            frames.end(),
            [&](const auto& frame) {
                return unchangedFrameIds.contains(frame.id());
            }),
        frames.end());

    db::eye::FrameGateway{txn}.updatex(frames);

    const auto updatedOIDs = collectKeys(oidToFrameIds);
    return {{updatedOIDs.begin(), updatedOIDs.end()}, frames.size()};
}

std::vector<std::uint64_t> collectSessionIds(const db::EyePanoramas& panoramas)
{
    std::set<std::uint64_t> sessionIds;
    for (const auto& panorama: panoramas) {
        sessionIds.insert(panorama.sessionId());
    }
    return {sessionIds.begin(), sessionIds.end()};
}

SesionIdToDevices loadExistingDevices(pqxx::transaction_base& txn,
                                      const std::vector<std::uint64_t>& sessionIds)
{
    const auto panoramaSessionToDevices =
        db::eye::PanoramaSessionToDeviceGateway{txn}.load(
            db::eye::table::PanoramaSessionToDevice::sessionId.in(
                sessionIds));

    db::TIds deviceIds;
    for (const auto& panoramaSessionToDevice: panoramaSessionToDevices) {
        deviceIds.push_back(panoramaSessionToDevice.deviceId());
    }

    const auto deviceById = byId(db::eye::DeviceGateway{txn}.load(
        db::eye::table::Device::id.in(deviceIds)));

    SesionIdToDevices sesionIdToDevices;
    for (const auto& panoramaSessionToDevice: panoramaSessionToDevices) {
        sesionIdToDevices[panoramaSessionToDevice.sessionId()].emplace(
            panoramaSessionToDevice.deviation(),
            deviceById.at(panoramaSessionToDevice.deviceId()));
    }

    return sesionIdToDevices;
}

db::eye::Device makePanoramaDevice()
{
    return db::eye::DeviceAttrs{
        db::eye::PanoramaDeviceAttrs{PANORAMA_FRAME_HORIZONTAL_FOV}};
}

PanoramaDevices makePanoramaDevices()
{
    PanoramaDevices panoramaDevices;
    for (auto frameDeviation: PANORAMA_FRAME_DEVIATIONS) {
        panoramaDevices.emplace(frameDeviation, makePanoramaDevice());
    }

    return panoramaDevices;
}

SesionIdToDevices makePanoramaDevices(
    const std::set<std::uint64_t>& sessionIds)
{
    SesionIdToDevices sesionIdToDevices;
    for (auto sessionId: sessionIds) {
        sesionIdToDevices[sessionId] = makePanoramaDevices();
    }
    return sesionIdToDevices;
}

void insertPanoramaDevices(pqxx::transaction_base& txn,
                           SesionIdToDevices& sesionIdToDevices)
{
    std::vector<db::eye::PanoramaSessionToDevice> panoramaSessionToDevices;

    for (auto& [sessionId, panoramaDevices]: sesionIdToDevices) {
        for (auto& [deviation, device]: panoramaDevices) {
            db::eye::DeviceGateway{txn}.insertx(device);

            panoramaSessionToDevices.emplace_back(
                sessionId, device.id(), deviation);
        }
    }

    db::eye::PanoramaSessionToDeviceGateway{txn}.insert(panoramaSessionToDevices);
}

SesionIdToDevices importDevices(pqxx::transaction_base& txn,
                                const db::EyePanoramas& panoramas)
{
    // Eye arranges series of frames by devices. This makes sense for photos
    // because current algorithms works better with series of photos made by
    // the same device. So, to make panorama frames behave the same way six
    // devices are created for each panorama session. Each device corresponds
    // to a frame direction in spite they all share the same camera matrix.

    const auto sessionIds = collectSessionIds(panoramas);

    SesionIdToDevices existingDevices = loadExistingDevices(txn, sessionIds);

    SesionIdToDevices newPanoramaDevices = makePanoramaDevices(
        difference(sessionIds, collectKeys(existingDevices)));

    insertPanoramaDevices(txn, newPanoramaDevices);

    existingDevices.merge(newPanoramaDevices);
    return existingDevices;
}

db::eye::PanoramaUrlContext makePanoramaUrlContext(const db::EyePanorama& panorama,
                                                   const geolib3::Degrees& deviation)
{
    // Note: the frame heading deviates CW from a vehicle course by
    // 'deviation' degrees.
    return {
        .oid = panorama.oid(),
        .heading = geolib3::normalize(panorama.vehicleCourse() + deviation.value()),
        .tilt = PANORAMA_FRAME_TILT,
        .horizontalFOV = PANORAMA_FRAME_HORIZONTAL_FOV,
        .size = PANORAMA_FRAME_SIZE};
}

struct PanoramaFrames {
    db::eye::Frames frames;
    std::vector<const db::EyePanorama*> panoramas;
};

PanoramaFrames makePanoramaFrames(const db::EyePanoramas& panoramas,
                                  const SesionIdToDevices& sessionIdToDevices)
{
    PanoramaFrames panoramaFrames;

    for (const auto& panorama: panoramas) {
        for (const auto& [deviation, device]:
             sessionIdToDevices.at(panorama.sessionId())) {

            panoramaFrames.panoramas.emplace_back(&panorama);

            panoramaFrames.frames.emplace_back(
                device.id(),
                PANORAMA_FRAME_ORIENTATION,
                makePanoramaUrlContext(panorama, deviation),
                PANORAMA_FRAME_SIZE,
                panorama.date());
        }
    }

    return panoramaFrames;
}

std::size_t importPanoramaFrames(pqxx::transaction_base& txn,
                                 PanoramaFrames panoramaFrames)
{
    const std::size_t newFramesCount = panoramaFrames.frames.size();

    db::eye::FrameGateway{txn}.insertx(panoramaFrames.frames);

    db::eye::PanoramaToFrames panoramaToFrames;
    db::eye::FrameLocations locations;
    db::eye::FramePrivacies privacies;

    panoramaToFrames.reserve(newFramesCount);
    locations.reserve(newFramesCount);
    privacies.reserve(newFramesCount);

    for (const auto& [panorama, frame]:
         Zip(panoramaFrames.panoramas, panoramaFrames.frames)) {
        const auto urlContext = frame.urlContext().panorama();

        panoramaToFrames.emplace_back(
            urlContext.oid,
            frame.id(),
            urlContext.heading,
            urlContext.tilt,
            urlContext.horizontalFOV,
            urlContext.size);

        locations.emplace_back(
            frame.id(),
            panorama->mercatorPos(),
            toRotation(urlContext.heading, PANORAMA_FRAME_ORIENTATION),
            toMoveVector(panorama->vehicleCourse()));

        privacies.emplace_back(frame.id(), db::FeaturePrivacy::Public);
    }

    db::eye::PanoramaToFrameGateway{txn}.insert(panoramaToFrames);
    db::eye::FrameLocationGateway{txn}.insertx(locations);
    db::eye::FramePrivacyGateway{txn}.insertx(privacies);

    return newFramesCount;
}

std::size_t import(pqxx::transaction_base& txn, db::EyePanoramas panoramas)
{
    if (panoramas.empty()) {
        INFO() << "Stop, no panorama updates!";
        return 0;
    }

    const auto framesUpdate = updateFrames(txn, panoramas);
    INFO() << "Updated panoramas " << framesUpdate.updatedOIDs.size();

    panoramas.erase(
        std::remove_if(
            panoramas.begin(),
            panoramas.end(),
            [&](const auto& panorama) {
                return framesUpdate.updatedOIDs.contains(panorama.oid());
            }),
        panoramas.end());
    INFO() << "New panoramas " << panoramas.size();

    const auto sessionIdToDevices = importDevices(txn, panoramas);

    const auto newFramesCount = importPanoramaFrames(
        txn, makePanoramaFrames(panoramas, sessionIdToDevices));

    return newFramesCount + framesUpdate.updatedFramesCount;
}

std::size_t import(pqxx::transaction_base& txn, const db::PanoramaOIDs& oids)
{
    return import(txn, db::EyePanoramaGateway{txn}.loadByIds(oids));
}

// Note: each panorama session is stored with its own transaction ID and the
//       order num is the most reliable field of their ordering within each session.
struct PanoramaTransactionId {
    db::TId txnId;
    std::uint64_t sessionId;
    std::uint32_t orderNum;
};

struct Batch {
    db::EyePanoramas panoramas;
    PanoramaTransactionId first;
    PanoramaTransactionId last;
};

Batch getNewBatch(pqxx::transaction_base& txn, std::size_t size)
{
    using table = db::table::EyePanorama;

    auto metadata = importPanoramaFrameMetadata(txn);

    const PanoramaTransactionId first = {
        metadata.getTxnId(), metadata.getSessionId(), metadata.getOrderNum()};

    const auto fromSameSession = table::txnId == first.txnId &&
                                 table::sessionId == first.sessionId &&
                                 table::orderNum > first.orderNum;

    const auto fromNextSession = table::txnId == first.txnId &&
                                 table::sessionId > first.sessionId;

    db::EyePanoramas panoramas = db::EyePanoramaGateway{txn}.load(
        fromSameSession || fromNextSession || table::txnId > first.txnId,
        sql_chemistry::limit(size)
            .orderBy(table::txnId)
            .orderBy(table::sessionId)
            .orderBy(table::orderNum));

    const PanoramaTransactionId last =
        panoramas.empty() ? first
                          : PanoramaTransactionId{
                                .txnId = panoramas.back().txnId(),
                                .sessionId = panoramas.back().sessionId(),
                                .orderNum = panoramas.back().orderNum()};

    return {std::move(panoramas), first, last};
};

std::ostream& operator<<(std::ostream& out, const PanoramaTransactionId& id)
{
    return out << id.txnId << ":" << id.sessionId << ":" << id.orderNum;
}

} // namespace

void ImportPanoramaFrame::processBatch(const db::PanoramaOIDs& oids)
{
    auto lock = lockIfNeed();
    auto txn = getMasterWriteTxn(*pool());

    import(*txn, oids);
    commitIfNeed(*txn);
}

bool ImportPanoramaFrame::processBatchInLoopMode(std::size_t size)
{
    auto lock = lockIfNeed();
    auto txn = getMasterWriteTxn(*pool());

    Batch batch = getNewBatch(*txn, size);
    INFO() << "Batch [" << batch.first << ", " << batch.last << ")";

    const std::size_t count = import(*txn, std::move(batch.panoramas));

    auto metadata = importPanoramaFrameMetadata(*txn);

    metadata.updateTxnId(batch.last.txnId);
    metadata.updateSessionId(batch.last.sessionId);
    metadata.updateOrderNum(batch.last.orderNum);
    metadata.updateTime();

    commitIfNeed(*txn);

    return count > 0;
}

} // namespace maps::mrc::eye
