#include "globals.h"
#include "utils.h"

#include <maps/wikimap/mapspro/services/mrc/libs/common/include/mds_path.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/common.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/walk_object_gateway.h>

#include <yandex/maps/proto/mrc/backoffice/backoffice.pb.h>
#include <yandex/maps/geolib3/proto.h>

#include <maps/infra/yacare/include/limit_rate.h>
#include <maps/infra/yacare/include/params/tvm.h>
#include <maps/infra/yacare/include/yacare.h>
#include <maps/libs/common/include/profiletimer.h>

#include <algorithm>

/// Here is the implementation of backoffice API that is described in
/// https://a.yandex-team.ru/arc/trunk/arcadia/maps/doc/proto/yandex/maps/proto/mrc/backoffice/README.md


namespace maps::mrc::agent_proxy {

namespace {

const std::string SELF_TVM_ALIAS = "mrc-agent-proxy";

struct BackofficePhoto {
    std::string image;
    geolib3::Point2 point;
    geolib3::Heading heading;
    chrono::TimePoint takenAt;
};

struct BackofficeObject {
    std::optional<db::Dataset> dataset;
    db::WalkFeedbackType type;
    db::GeometryVariant geometry;
    std::string comment;
    std::vector<BackofficePhoto> photos;
    std::optional<std::string> nmapsObjectId;
};

db::GeometryVariant decodeGeometryVariant(
    const yandex::maps::proto::mrc::backoffice::CreateObjectRequest&
        createObjectRequest)
{
    switch (auto geometryCase = createObjectRequest.geometry_case()) {
        using yandex::maps::proto::mrc::backoffice::CreateObjectRequest;
        case CreateObjectRequest::kPoint:
            return geolib3::proto::decode(createObjectRequest.point());
        case CreateObjectRequest::kPolyline:
            return geolib3::proto::decode(createObjectRequest.polyline());
        case CreateObjectRequest::kPolygon:
            return geolib3::proto::decode(createObjectRequest.polygon());
        default:
            throw yacare::errors::BadRequest()
                << "Unexpected geometry case: " << geometryCase;
    }
}

db::Dataset decodeDataset(
    yandex::maps::proto::mrc::backoffice::Dataset dataset)
{
    switch (dataset) {
        case yandex::maps::proto::mrc::backoffice::ALTAY_PEDESTRIANS:
            return db::Dataset::AltayPedestrians;
        case yandex::maps::proto::mrc::backoffice::YANG_PEDESTRIANS:
            return db::Dataset::YangPedestrians;
        case yandex::maps::proto::mrc::backoffice::NEXAR_DASHCAMS:
            return db::Dataset::NexarDashcams;
        case yandex::maps::proto::mrc::backoffice::TOLOKA_PEDESTRIANS:
            return db::Dataset::TolokaPedestrians;
        default:
            REQUIRE(false, "Unsupported dataset value: " << dataset);
    }
}

BackofficePhoto decodeBackofficePhoto(
    const yandex::maps::proto::mrc::backoffice::GeoPhoto& geoPhoto)
{
    auto result = BackofficePhoto{};
    result.image = geoPhoto.image();
    auto& shootingPoint = geoPhoto.shooting_point();
    result.point = geolib3::proto::decode(shootingPoint.point().point());
    result.heading = geolib3::Heading(shootingPoint.direction().azimuth());
    result.takenAt = chrono::convertFromUnixTime(geoPhoto.taken_at());
    return result;
}


BackofficeObject decodeBackofficeObject(
    const yandex::maps::proto::mrc::backoffice::CreateObjectRequest&
        createObjectRequest)
{
    auto result = BackofficeObject{};
    result.type = decodeFeedbackType(
        createObjectRequest.has_type()
            ? createObjectRequest.type()
            : yandex::maps::proto::mrc::backoffice::UNKNOWN_OBJECT_TYPE);
    result.geometry = decodeGeometryVariant(createObjectRequest);
    if (createObjectRequest.has_comment()) {
        result.comment = createObjectRequest.comment();
    }
    if (createObjectRequest.has_dataset()) {
        result.dataset = decodeDataset(createObjectRequest.dataset());
    }
    auto photosNumber = createObjectRequest.photos().size();
    CHECK_REQ(0 < photosNumber, "No photos");
    for (auto& geoPhoto : createObjectRequest.photos()) {
        result.photos.push_back(decodeBackofficePhoto(geoPhoto));
    }
    if (createObjectRequest.has_nmaps_object_id()) {
        result.nmapsObjectId = createObjectRequest.nmaps_object_id();
    }
    return result;
}


db::WalkObject saveBackofficeObject(const BackofficeObject& backofficeObject,
                                    db::Dataset defaultDataset,
                                    const std::string& sourceId,
                                    pqxx::transaction_base& txn)
{
    CHECK_REQ(!backofficeObject.photos.empty(), "No photos");
    auto takenAt = std::min_element(backofficeObject.photos.begin(),
                                    backofficeObject.photos.end(),
                                    [](auto& lhs, auto& rhs) {
                                        return lhs.takenAt < rhs.takenAt;
                                    })
                       ->takenAt;
    auto result = db::WalkObject{backofficeObject.dataset.value_or(defaultDataset),
                                 sourceId,
                                 takenAt,
                                 backofficeObject.type,
                                 {},
                                 backofficeObject.comment};
    if (backofficeObject.nmapsObjectId) {
        result.setNmapsObjectId(*backofficeObject.nmapsObjectId);
    }
    std::visit(
        [&result](const auto& geom) { result.setGeodeticGeometry(geom); },
        backofficeObject.geometry);
    db::WalkObjectGateway{txn}.insertx(result);
    return result;
}

db::Feature saveBackofficePhoto(const BackofficePhoto& backofficePhoto,
                                std::optional<db::TId> objectId,
                                db::Dataset dataset,
                                const std::string& sourceId,
                                pqxx::transaction_base& txn,
                                mds::Mds& mdsClient)
{
    auto result = sql_chemistry::GatewayAccess<db::Feature>::construct()
                      .setGeodeticPos(backofficePhoto.point)
                      .setHeading(backofficePhoto.heading)
                      .setTimestamp(backofficePhoto.takenAt)
                      .setSourceId(sourceId)
                      .setDataset(dataset)
                      .setUploadedAt(chrono::TimePoint::clock::now());
    if (objectId.has_value()) {
        result.setWalkObjectId(objectId.value());
    }

    auto image = common::decodeImage(backofficePhoto.image);
    result.setSize(image.cols, image.rows);

    db::FeatureGateway(txn).insert(result);

    auto pt = ProfileTimer{};
    auto mdsPath = common::makeMdsPath(
        common::MdsObjectSource::Walk,
        common::MdsObjectType::Image,
        result.id());
    auto mdsResponse = mdsClient.post(mdsPath, backofficePhoto.image);
    auto key = mdsResponse.key();
    result.setMdsKey(key);
    INFO() << "Uploading photo to MDS took " << pt << " seconds. "
           << "Mds key: " << key.groupId << "/" << key.path;

    db::FeatureGateway(txn).update(result, db::UpdateFeatureTxn::No);
    return result;
}

auto makeCreateObjectResponse(const db::Features& features)
    -> yandex::maps::proto::mrc::backoffice::CreateObjectResponse
{
    auto result = yandex::maps::proto::mrc::backoffice::CreateObjectResponse{};
    for (const auto& feature : features) {
        *result.add_photo_ids() = std::to_string(feature.id());
    }
    return result;
}

auto makeCreatePhotoResponse(const db::Feature& feature)
    -> yandex::maps::proto::mrc::backoffice::CreatePhotoResponse
{
    auto result = yandex::maps::proto::mrc::backoffice::CreatePhotoResponse{};
    *result.mutable_photo_id() = std::to_string(feature.id());
    return result;
}

void write(const google::protobuf::Message& message)
{
    ASSERT(message.IsInitialized());
    auto& response = yacare::response();
    auto buf = TString{};
    bool success = message.SerializeToString(&buf);
    response.setHeader(CONTENT_TYPE, CONTENT_TYPE_PROTOBUF);
    REQUIRE(success, "failed to serialize protobuf");
    response.write(buf.data(), buf.size());
}

} // namespace

YCR_RESPOND_TO("POST /v2/objects/create",
               YCR_USING(yacare::Tvm2ServiceRequire(SELF_TVM_ALIAS)),
               YCR_LIMIT_RATE(resource("mrc_agent_proxy/v2/objects/create")),
               YCR_USING(yacare::NginxLimitBody<yacare::ConfigScope::Endpoint>{}.maxSize(30_MB)))
{
    auto createObjectRequest =
        yandex::maps::proto::mrc::backoffice::CreateObjectRequest{};
    CHECK_REQ(createObjectRequest.ParseFromString(TString{request.body()}),
              "Invalid object request");
    auto backofficeObject = decodeBackofficeObject(createObjectRequest);

    auto defaultDataset = db::Dataset::BackofficeObject;
    auto sourceId = db::feature::NO_SOURCE_ID;
    auto txn = Globals::pool().masterWriteableTransaction();
    auto& mdsClient = Globals::mdsClient();

    auto features = db::Features{};
    try {
        auto walkObject =
            saveBackofficeObject(backofficeObject, defaultDataset, sourceId, *txn);
        for (const auto& backofficePhoto : backofficeObject.photos) {
            features.push_back(saveBackofficePhoto(backofficePhoto,
                                                   walkObject.id(),
                                                   walkObject.dataset(),
                                                   sourceId,
                                                   *txn,
                                                   mdsClient));
        }
        txn->commit();
    }
    catch (const std::exception&) {
        INFO() << "Remove saved images from MDS";
        for (const auto& feature : features) {
            mdsClient.del(feature.mdsKey());
        }
        throw;
    }
    write(makeCreateObjectResponse(features));
}

YCR_RESPOND_TO("POST /v2/photos/create",
               YCR_USING(yacare::Tvm2ServiceRequire(SELF_TVM_ALIAS)),
               YCR_LIMIT_RATE(resource("mrc_agent_proxy/v2/photos/create")),
               YCR_USING(yacare::NginxLimitBody<yacare::ConfigScope::Endpoint>{}.maxSize(30_MB)))
{
    auto createPhotoRequest =
        yandex::maps::proto::mrc::backoffice::CreatePhotoRequest{};
    CHECK_REQ(createPhotoRequest.ParseFromString(TString{request.body()}),
              "Invalid photo request");
    auto backofficePhoto = decodeBackofficePhoto(createPhotoRequest.photo());

    auto dataset = db::Dataset::BackofficePhoto;
    if (createPhotoRequest.has_dataset()) {
        dataset = decodeDataset(createPhotoRequest.dataset());
    }
    auto sourceId = db::feature::NO_SOURCE_ID;
    auto txn = Globals::pool().masterWriteableTransaction();
    auto& mdsClient = Globals::mdsClient();

    auto feature = std::optional<db::Feature>{};
    try {
        feature = saveBackofficePhoto(backofficePhoto,
                                      std::nullopt /*objectId*/,
                                      dataset,
                                      sourceId,
                                      *txn,
                                      mdsClient);
        txn->commit();
    }
    catch (const std::exception&) {
        if (feature) {
            INFO() << "Remove saved image from MDS";
            mdsClient.del(feature->mdsKey());
        }
        throw;
    }
    write(makeCreatePhotoResponse(*feature));
}


} // namespace maps::mrc::agent_proxy
