#include "fixture.h"

#include <maps/wikimap/mapspro/services/mrc/eye/lib/import_panorama_frame/include/metadata.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/eye/frame_gateway.h>

#include <maps/libs/geolib/include/direction.h>
#include <maps/libs/geolib/include/distance.h>

namespace maps::mrc::eye::tests {

namespace {

const auto LOAD_PANORAMAS_SINCE =
    chrono::parseSqlDateTime("2020-01-01 00:00:00");

constexpr double PANORAMA_DISTRIBUTION_METERS = 30.;
constexpr geolib3::Point2 MOSCOW_CENTRE{37.622513, 55.753220};

auto& playground() {
    static unittest::WithUnittestConfig<unittest::DatabaseFixture> global;
    return global;
}

auto& poolHolder()
{
    static auto global = playground().config().makePoolHolder();
    return global;
}

ImportPanoramaFrameConfig workerConfig()
{
    return {
        .mrc = {
            .pool = &poolHolder().pool(),
            .commit = true,
            .lockFree = true}};
}

auto newTxn()
{
    return playground().pool().masterWriteableTransaction();
}

db::PanoramaOID makePanoramaOID(
    std::uint64_t sessionId, std::uint32_t orderNum)
{
    return std::to_string(sessionId) + "_" + std::to_string(orderNum) + "_oid";
}

geolib3::Point2 shift(const db::EyePanorama* panorama,
                      double meters = PANORAMA_DISTRIBUTION_METERS)
{
    return geolib3::fastGeoShift(
        panorama->geodeticPos(),
        geolib3::Direction2{panorama->vehicleCourse()}.vector() * meters);
}

} // namespace

ImportPanoramaFrameFixture::ImportPanoramaFrameFixture()
{
    playground().postgres().truncateTables();
}

void ImportPanoramaFrameFixture::makeEyePanoramas(
    std::uint64_t sessionId, std::size_t num)
{
    ASSERT(num > 0);

    const auto nextPanorama = [&](const db::EyePanoramas& panoramas) {
        using namespace std::literals::chrono_literals;

        db::EyePanorama const* lastInSession = nullptr;
        for (const auto& panorama: panoramas) {
            if (panorama.sessionId() != sessionId) {
                continue;
            }

            if (!lastInSession ||
                lastInSession->orderNum() < panorama.orderNum()) {
                lastInSession = &panorama;
            }
        }

        if (lastInSession) {
            return db::EyePanorama{
                makePanoramaOID(sessionId, lastInSession->orderNum() + 1),
                lastInSession->date() + 1s,
                sessionId,
                lastInSession->orderNum() + 1,
                shift(lastInSession),
                lastInSession->vehicleCourse(),
                false /* deleted */};
        } else {
            return db::EyePanorama{
                makePanoramaOID(sessionId, 0),
                LOAD_PANORAMAS_SINCE /* date */,
                sessionId,
                std::uint32_t{0} /* order num */,
                MOSCOW_CENTRE /* geodetic position */,
                geolib3::Heading{double(std::rand() % 360)} /* vehicle course */,
                false /* deleted */};
        }
    };

    db::EyePanoramas panoramas{nextPanorama(getState().panoramas)};
    for (; num > 1; --num) {
        panoramas.push_back(nextPanorama(panoramas));
    }

    auto txn = newTxn();
    db::EyePanoramaGateway{*txn}.insertx(panoramas);
    txn->commit();

    dirty_ = true;
}

void ImportPanoramaFrameFixture::setDeleted(const db::PanoramaOIDs& oids,
                                            bool deleted)
{
    const auto state = getState();
    db::EyePanoramas panoramas;

    std::copy_if(
        state.panoramas.begin(),
        state.panoramas.end(),
        std::back_inserter(panoramas),
        [&](const auto& panorama) {
            return std::find(oids.begin(), oids.end(), panorama.oid()) !=
                   oids.end();
        });

    for (auto& panorama: panoramas) {
        panorama.setDeleted(deleted);
    }

    auto txn = newTxn();
    db::EyePanoramaGateway{*txn}.updatex(panoramas);
    txn->commit();

    dirty_ = true;
}

ImportPanoramaFrame ImportPanoramaFrameFixture::importPanoramaFrame() const
{
    return workerConfig();
}

const ImportPanoramaFrameFixture::State& ImportPanoramaFrameFixture::getState()
{
    if (isDirty()) {
        reloadState();
    }

    return state_;
}

bool ImportPanoramaFrameFixture::isDirty() const
{
    const auto hasMetadataBeenChanged = [this] {
        auto txn = newTxn();
        auto metadataMgr = importPanoramaFrameMetadata(*txn);

        return metadataMgr.getTxnId() != state_.metadata.frameTxnId ||
               metadataMgr.getSessionId() != state_.metadata.sessionId ||
               metadataMgr.getOrderNum() != state_.metadata.orderNum;
    };

    return dirty_ || hasMetadataBeenChanged();
}

void ImportPanoramaFrameFixture::reloadState() {
    using namespace sql_chemistry;
    using namespace db::eye;

    const auto framesJoin =
        table::Frame::id == table::PanoramaToFrame::frameId &&
        table::Frame::deviceId == table::Device::id &&
        table::Device::id == table::PanoramaSessionToDevice::deviceId &&
        table::PanoramaToFrame::oid == db::table::EyePanorama::oid &&
        table::PanoramaToFrame::frameId == table::FrameLocation::frameId;


    const auto framesOrdering =
        orderBy(db::table::EyePanorama::sessionId)
            .orderBy(db::table::EyePanorama::sessionId)
            .orderBy(table::PanoramaSessionToDevice::deviation)
            .asc();

    auto txn = newTxn();
    auto metadataMgr = importPanoramaFrameMetadata(*txn);

    State state = {
        .panoramas = db::EyePanoramaGateway{*txn}.load(
            orderBy(db::table::EyePanorama::sessionId)
                .orderBy(db::table::EyePanorama::orderNum)
                .asc()),

        .panoramaSessionToDevices = PanoramaSessionToDeviceGateway{*txn}.load(
            table::PanoramaSessionToDevice::deviceId == table::Device::id,
            orderBy(table::PanoramaSessionToDevice::sessionId)
                .orderBy(table::PanoramaSessionToDevice::deviation)
                .asc()),

        .devices = DeviceGateway{*txn}.load(
            table::PanoramaSessionToDevice::deviceId == table::Device::id,
            orderBy(table::PanoramaSessionToDevice::sessionId)
                .orderBy(table::PanoramaSessionToDevice::deviation)
                .asc()),

        .panoramaToFrames = PanoramaToFrameGateway{*txn}.load(framesJoin,
                                                              framesOrdering),

        .frames = FrameGateway{*txn}.load(framesJoin, framesOrdering),

        .locations = FrameLocationGateway{*txn}.load(framesJoin, framesOrdering),

        .metadata = {
            .frameTxnId = metadataMgr.getTxnId(),
            .sessionId = metadataMgr.getSessionId(),
            .orderNum = metadataMgr.getOrderNum()}};

    // Fill the mappings
    const auto findRelatedEntity = [](const auto& entities, auto key)
        -> typename std::remove_reference_t<decltype(entities)>::const_pointer {
        const auto it = std::find_if(
            entities.begin(), entities.end(), [&](const auto& entity) {
                return key(entity);
            });
        if (it == entities.end()) {
            return nullptr;
        }
        return &*it;
    };

    for (const auto& [p2f, frame]: Zip(state.panoramaToFrames, state.frames)) {
        const db::TId frameId = frame.id();

        const auto* panorama = findRelatedEntity(
            state.panoramas, [oid = p2f.oid()](const auto& panorama) {
                return panorama.oid() == oid;
            });

        const auto* device = findRelatedEntity(
            state.devices,
            [deviceId = frame.deviceId()](const auto& device) {
                return device.id() == deviceId;
            });

        const auto* ps2d = findRelatedEntity(
            state.panoramaSessionToDevices,
            [deviceId = device->id()](const auto& ps2d) {
                return ps2d.deviceId() == deviceId;
            });

        state.frameIdToPanorama[frameId] = panorama;
        state.frameIdToDevice[frameId] = device;
        state.frameIdToPanoramaSessionToDevice[frameId] = ps2d;
    }

    state_ = std::move(state);
    dirty_ = false;
}

} // namespace maps::mrc::eye::tests
