#include <maps/wikimap/mapspro/services/mrc/eye/lib/import_mrc/include/import.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/import_mrc/include/metadata.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/location/include/move.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/location/include/rotation.h>

#include <maps/wikimap/mapspro/services/mrc/libs/common/include/exif.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/frame_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_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/units.h>

#include <maps/libs/json/include/value.h>
#include <maps/libs/log8/include/log8.h>

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

#include <set>

namespace maps::mrc::eye {

using namespace db::eye;

namespace {


SourecIdToDevice loadSourceIdToDeviceMap(
        pqxx::transaction_base& txn,
        const std::vector<std::string>& sourceIds)
{
    std::unordered_map<db::TId, std::string> deviceIdToSourceId;

    auto filter = table::FeatureSourceToDevice::sourceId.in(sourceIds);
    for (auto&& pair: FeatureSourceToDeviceGateway(txn).load(filter)) {
        deviceIdToSourceId.emplace(pair.deviceId(), pair.sourceId());
    }

    SourecIdToDevice sourceIdToDevice;
    for (auto&& device: DeviceGateway(txn).loadByIds(collectKeys(deviceIdToSourceId))) {
        sourceIdToDevice.emplace(deviceIdToSourceId.at(device.id()), device);
    }

    return sourceIdToDevice;
}

std::unordered_map<db::TId, db::TId> loadFeatureIdToFrameIdMap(
        pqxx::transaction_base& txn,
        const db::TIds& featureIds)
{
    std::unordered_map<db::TId, db::TId> featureIdToFrameId;

    const auto filter = table::FeatureToFrame::featureId.in(featureIds);
    for (auto&& pair: FeatureToFrameGateway(txn).load(filter)) {
        featureIdToFrameId.emplace(pair.featureId(), pair.frameId());
    }

    return featureIdToFrameId;
}

Eigen::Quaterniond makeRotation(const db::Feature& feature)
{
    if (feature.hasCameraRodrigues()) {
        const auto rotation = toRotation(feature.cameraRodrigues());
        const auto orientation = decomposeRotation(rotation).orientation;

        if (feature.orientation() == orientation) {
            return rotation;
        }

        ERROR() << "Feature " << feature.id() << " has conflicting orientation fields!";
        ERROR() << "Camera rodrigues orientation: " << orientation;
        ERROR() << "Image orientation: " << feature.orientation();
    }

    return toRotation(feature.heading(), feature.orientation());
}

inline common::ImageOrientation makeOrientation(const db::Feature& feature)
{
    return feature.orientation(); // force to use feature orientation
}

template<typename FeatureRange>
size_t updateFrames(
        pqxx::transaction_base& txn,
        const FeatureRange& features,
        const std::unordered_map<db::TId, db::TId>& featureIdToframeId)
{
    static constexpr double posEpsMeters{0.3};
    static constexpr geolib3::Degrees angleEps{5};

    std::unordered_set<db::TId> updatedIds;

    std::unordered_map<db::TId, const db::Feature*> frameIdToFeature;
    for (const auto& feature: features) {
        frameIdToFeature.emplace(featureIdToframeId.at(feature.id()), &feature);
    }

    const db::TIds frameIds = collectValues(featureIdToframeId);
    db::TIds notDeletedFrameIds;

    Frames updatexFrames;
    Frames updateFrames;
    for (auto&& frame: FrameGateway(txn).loadByIds(frameIds)) {
        const db::Feature* feature = frameIdToFeature.at(frame.id());
        bool updated = false;
        bool shouldUpdateTxn = false;

        const bool deleted = not feature->isPublished();

        if (deleted != frame.deleted()) {
            frame.setDeleted(deleted);
            updated = true;
            shouldUpdateTxn = true;
        }

        if (not deleted) {
            notDeletedFrameIds.push_back(frame.id());

            const common::ImageOrientation orientation = makeOrientation(*feature);
            if (orientation != frame.orientation()) {
                frame.setOrientation(orientation);
                updated = true;
                shouldUpdateTxn = true;
            }

            const auto urlContext = frame.urlContext().mrc();
            if (urlContext.mdsGroupId != feature->mdsGroupId() ||
                    urlContext.mdsPath != feature->mdsPath())
            {
                frame.setUrlContext(db::eye::MrcUrlContext{
                    feature->id(), feature->mdsGroupId(), feature->mdsPath()});
                updated = true;
            }
        }

        if (updated) {
            updatedIds.insert(frame.id());
            if (shouldUpdateTxn) {
                updatexFrames.push_back(std::move(frame));
            } else {
                updateFrames.push_back(frame);
            }
        }
    }

    if (!updatexFrames.empty()) {
        FrameGateway(txn).updatex(updatexFrames);
    }

    if (!updateFrames.empty()) {
        FrameGateway(txn).updateWithoutX(updateFrames);
    }

    FrameLocations locations;
    for (auto&& location: FrameLocationGateway(txn).loadByIds(notDeletedFrameIds)) {
        const db::Feature* feature = frameIdToFeature.at(location.frameId());

        bool updated = false;

        if (geolib3::geoDistance(location.geodeticPos(), feature->geodeticPos()) > posEpsMeters) {
            location.setMercatorPos(feature->mercatorPos());
            updated = true;
        }

        const Eigen::Quaterniond rotation = makeRotation(*feature);
        if (absDiffInDegrees(location.rotation(), rotation) > angleEps) {
            location.setRotation(rotation);
            updated = true;
        }

        const Eigen::Vector3d move = toMoveVector(feature->heading());
        if (absDiffInDegrees(location.move(), move) > angleEps) {
            location.setMove(move);
            updated = true;
        }

        if (updated) {
            updatedIds.insert(location.frameId());
            locations.push_back(location);
        }
    }

    FrameLocationGateway(txn).updatex(locations);


    FramePrivacies privacies;
    for (auto&& privacy: FramePrivacyGateway(txn).loadByIds(notDeletedFrameIds)) {
        const db::Feature* feature = frameIdToFeature.at(privacy.frameId());

        if (privacy.type() != feature->privacy()) {
            privacy.setType(feature->privacy());
            updatedIds.insert(privacy.frameId());
            privacies.push_back(privacy);
        }
    }

    FramePrivacyGateway(txn).updatex(privacies);

    return updatedIds.size();
}

template<typename FeatureRange>
size_t insertNewFrames(
        pqxx::transaction_base& txn,
        const FeatureRange& features,
        const SourecIdToDevice& sourceIdToDevice)
{
    Frames frames;
    for (const auto& feature: features) {
        frames.emplace_back(
            sourceIdToDevice.at(feature.sourceId()).id(),
            makeOrientation(feature),
            db::eye::MrcUrlContext{feature.id(), feature.mdsGroupId(), feature.mdsPath()},
            feature.size(),
            feature.timestamp()
        );
    }

    FrameGateway(txn).insertx(frames);

    FrameLocations locations;
    for (const auto& [frame, feature]: Zip(frames, features)) {
        locations.emplace_back(
            frame.id(),
            feature.mercatorPos(),
            makeRotation(feature),
            toMoveVector(feature.heading())
        );
    }

    FrameLocationGateway(txn).insertx(locations);

    FramePrivacies privacies;
    for (const auto& [frame, feature]: Zip(frames, features)) {
        privacies.emplace_back(frame.id(), feature.privacy());
    }

    FramePrivacyGateway(txn).insertx(privacies);

    std::vector<FeatureToFrame> matching;
    for (const auto& [feature, frame]: Zip(features, frames)) {
        matching.emplace_back(feature.id(), frame.id());
    }

    FeatureToFrameGateway(txn).insert(matching);

    return frames.size();
}

struct FeatureTransactionId {
    db::TId featureId;
    db::TId txnId;
};

std::ostream& operator<<(std::ostream& out, const FeatureTransactionId& id)
{
    return out << id.txnId << ":" << id.featureId;
}

struct Batch {
    db::TIds featureIds;
    FeatureTransactionId begin;
    FeatureTransactionId end;
};

Batch getNewBatch(pqxx::transaction_base& txn, size_t size)
{
    auto metadata = importMrcMetadata(txn);
    const FeatureTransactionId begin = {metadata.getFeatureId(), metadata.getTxnId()};

    using table = db::table::FeatureTransaction;

    auto pairs = db::FeatureTransactionGateway(txn).load(
        table::transactionId == begin.txnId and table::featureId >= begin.featureId,
        sql_chemistry::limit(size)
            .orderBy(table::transactionId)
            .orderBy(table::featureId)
    );

    if (pairs.size() < size) {
        auto pairsByTxnId = db::FeatureTransactionGateway(txn).load(
            table::transactionId > begin.txnId,
            sql_chemistry::limit(size - pairs.size())
                .orderBy(table::transactionId)
                .orderBy(table::featureId)
        );

        pairs.insert(pairs.end(),
            pairsByTxnId.begin(), pairsByTxnId.end()
        );
    }

    if (pairs.empty()) {
        return {{}, begin, begin};
    }

    db::TIds featureIds;
    for (const auto& pair: pairs) {
        featureIds.push_back(pair.featureId());
    }

    const auto& last = pairs.back();
    const FeatureTransactionId end = {last.featureId() + 1, last.transactionId()};

    return {featureIds, begin, end};
}

} // namespace

std::optional<std::string> ImportMrc::getCameraModel(const db::Feature& feature)
{
    return common::parseModelFromExif(config_.mds->get(feature.mdsKey()));
}

SourecIdToDevice ImportMrc::importDevices(pqxx::transaction_base& txn, const db::Features& features)
{
    std::unordered_map<std::string, const db::Feature*> sourceIdToFeature;
    for (const auto& feature: features) {
        if (feature.isPublished()) {
            sourceIdToFeature.emplace(feature.sourceId(), &feature);
        }
    }

    std::vector<std::string> sourceIds = collectKeys(sourceIdToFeature);
    auto sourceIdToDevice = loadSourceIdToDeviceMap(txn, sourceIds);

    sourceIds.erase(
        std::remove_if(
            sourceIds.begin(), sourceIds.end(),
            [&](const auto& sourceId) { return sourceIdToDevice.count(sourceId); }
        ),
        sourceIds.end()
    );

    Devices devices;
    for (const auto& sourceId: sourceIds) {
        const auto model = getCameraModel(*sourceIdToFeature.at(sourceId));
        devices.emplace_back(db::eye::MrcDeviceAttrs{model});
    }

    INFO() << "Create devices " << devices.size();

    DeviceGateway(txn).insertx(devices);

    INFO() << "Create device matching...";

    std::vector<FeatureSourceToDevice> matching;
    for (const auto& [sourceId, device]: Zip(sourceIds, devices)) {
        matching.emplace_back(sourceId, device.id());
    }

    FeatureSourceToDeviceGateway(txn).insert(matching);

    for (auto&& [sourceId, device]: Zip(sourceIds, devices)) {
        sourceIdToDevice.emplace(sourceId, device);
    }

    return sourceIdToDevice;
}

size_t ImportMrc::import(pqxx::transaction_base& txn, const db::TIds& featureIds)
{
    if (featureIds.empty()) {
        INFO() << "Stop, no features!";
        return 0;
    }

    const db::Features features = db::FeatureGateway(txn).loadByIds(featureIds);
    INFO() << "Load features " << features.size();

    const db::TIds importIds = collectIds(features);

    const auto sourceIdToDevice = importDevices(txn, features);
    const auto featureIdToFrameId = loadFeatureIdToFrameIdMap(txn, importIds);

    auto isOld= [&](const auto& feature) {
        return featureIdToFrameId.count(feature.id());
    };

    const size_t updated = updateFrames(txn, MakeFilteringRange(features, isOld), featureIdToFrameId);

    INFO() << "Updated " << updated << " features";

    auto isNew = [&](const auto& feature) {
        return not featureIdToFrameId.count(feature.id()) and feature.isPublished();
    };

    const size_t inserted = insertNewFrames(txn, MakeFilteringRange(features, isNew), sourceIdToDevice);
    INFO() << "Create " << inserted << " features";

    return inserted + updated;
}

void ImportMrc::processBatch(const db::TIds& featureIds)
{
    auto lock = lockIfNeed();
    auto txn = getMasterWriteTxn(*(config_.mrc.pool));

    if (featureIds.empty()) {
        INFO() << "Stop, no features!";
        return;
    }

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

bool ImportMrc::processBatchInLoopMode(size_t size)
{
    auto lock = lockIfNeed();
    auto txn = getMasterWriteTxn(*(config_.mrc.pool));

    const Batch batch = getNewBatch(*txn, size);
    INFO() << "Batch [" << batch.begin << ", " << batch.end << ")";

    import(*txn, batch.featureIds);

    auto metadata = importMrcMetadata(*txn);
    metadata.updateTxnId(batch.end.txnId);
    metadata.updateFeatureId(batch.end.featureId);
    metadata.updateTime();

    commitIfNeed(*txn);

    return batch.featureIds.size() > 0;
}

} // namespace maps::mrc::eye
