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

#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/pgpool/include/pgpool3.h>

#include <maps/libs/common/include/file_utils.h>

#include <library/cpp/string_utils/base64/base64.h>

#include <opencv2/opencv.hpp>

#include <fstream>
#include <iostream>
#include <sstream>
#include <vector>

namespace {

static const std::string JSON_NAME_FEATURES = "features";
static const std::string JSON_NAME_SOURCE_ID = "sourceId";
static const std::string JSON_NAME_GEODETIC_POS = "geodeticPos";
static const std::string JSON_NAME_HEADING = "heading";
static const std::string JSON_NAME_TIMESTAMP = "timestamp";
static const std::string JSON_NAME_DATASET = "dataset";
static const std::string JSON_NAME_ORIENTATION = "orientation";
static const std::string JSON_NAME_IMAGE = "image";
static const std::string JSON_NAME_IMAGE_URL = "imageUrl";
static const std::string JSON_NAME_IMAGE_PATH = "imagePath";

struct FeatureWithImages {
    maps::mrc::db::Features features;
    std::vector<std::string> images;
};

std::string download(maps::http::Client& client, const std::string& url) {
    INFO() << "Download from " << url;
    maps::common::RetryPolicy retryPolicy;
    retryPolicy.setTryNumber(10)
        .setInitialCooldown(std::chrono::seconds(1))
        .setCooldownBackoff(2);

    auto validateResponse = [](const auto& maybeResponse) {
        return maybeResponse.valid() && maybeResponse.get().responseClass() != maps::http::ResponseClass::ServerError;
    };
    auto resp = maps::common::retry(
                [&]() {
                    return maps::http::Request(client, maps::http::GET, maps::http::URL(url)).perform();
                },
                retryPolicy,
                validateResponse
            );
    REQUIRE(resp.responseClass() == maps::http::ResponseClass::Success,
        "Unexpected response status " << resp.status() << " for url "
        << url);
    return resp.readBody();
}

std::string base64Decode(const std::string& src) {
    std::string dst(Base64DecodeBufSize(src.length()), '\0');
    dst.resize(Base64Decode(dst.data(), src.begin(), src.end()));
    return dst;
}

/*
    Json format:
    {
        features: [
            {
                "sourceId" : "string", # some string with source id
                "geodeticPos" : [,],   # geodetic position
                "heading" : 0.0,       # floating point value
                "timestamp" : 2018-05-10 15:57:26.018+03,
                "dataset" : "fake",    # one of: "agents", "rides", "walks"
                "orientation" : 2,     # image orientation
                "image"   : "..."      # image data in base64
                "imageUrl" : "..."     # url to file with image (used if "image" params doesn't exists)
                "imagePath": "..."     # relative path to image
            },
            {
                . . .
            },
            . . .
            {
                . . .
            }
        ]
    }
*/
FeatureWithImages loadFeaturesFromJson(const std::string& path) {
    FeatureWithImages result;
    maps::json::Value fileJson = maps::json::Value::fromFile(path);

    maps::http::Client client;
    maps::json::Value featuresJson = fileJson[JSON_NAME_FEATURES];
    for (size_t i = 0; i < featuresJson.size(); i++) {
        const maps::json::Value& featureJson = featuresJson[i];
        result.features.emplace_back(
            featureJson[JSON_NAME_SOURCE_ID].as<std::string>(),
            maps::geolib3::Point2(
                featureJson[JSON_NAME_GEODETIC_POS][0].as<double>(),
                featureJson[JSON_NAME_GEODETIC_POS][1].as<double>()),
            maps::geolib3::Heading(featureJson[JSON_NAME_HEADING].as<double>()),
            featureJson[JSON_NAME_TIMESTAMP].as<std::string>(),
            maps::mds::Key("", ""),
            maps::enum_io::fromString<maps::mrc::db::Dataset>(featureJson[JSON_NAME_DATASET].as<std::string>())
        ).setOrientation(
            maps::mrc::common::ImageOrientation::fromExif(featureJson[JSON_NAME_ORIENTATION].as<int>()))
            .setPrivacy(maps::mrc::db::FeaturePrivacy::Public);

        if (featureJson.hasField(JSON_NAME_IMAGE))
            result.images.push_back(
                base64Decode(featureJson[JSON_NAME_IMAGE].as<std::string>()));
        else if (featureJson.hasField(JSON_NAME_IMAGE_URL)){
            result.images.push_back(
                download(client, featureJson[JSON_NAME_IMAGE_URL].as<std::string>()));
        } else {
            result.images.push_back(
                ::maps::common::readFileToString(featureJson[JSON_NAME_IMAGE_PATH].as<std::string>())
            );
        }

        cv::Mat buffer(result.images.back().size(), 1, CV_8UC1, result.images.back().data());
        cv::Mat image = cv::imdecode(buffer, cv::IMREAD_IGNORE_ORIENTATION);
        result.features.back().setSize(image.cols, image.rows);
    }
    return result;
}

void uploadFeatures(
    maps::pgpool3::Pool& pool,
    maps::mds::Mds &mds,
    maps::mrc::db::Features &features,
    const std::vector<std::string> &images
)
{
    REQUIRE(features.size() == images.size(),
        "Different amount of features and images");

    auto txn = pool.masterWriteableTransaction();
    maps::mrc::db::FeatureGateway featureGateway(*txn);
    featureGateway.insert(features);

    for (size_t i = 0; i < features.size(); i++) {
        std::string mdsPath = maps::mrc::common::makeMdsPath(
            maps::mrc::common::MdsObjectSource::Imported,
            maps::mrc::common::MdsObjectType::Image,
            features[i].id());

        maps::mds::PostResponse resp = mds.post(mdsPath, images[i]);

        features[i].setMdsKey(resp.key());

        INFO() << "Add feature id: " << std::to_string(features[i].id())
               << " image url: " << mds.makeReadUrl(resp.key());
    }
    featureGateway.update(features, maps::mrc::db::UpdateFeatureTxn::Yes);
    txn->commit();
}

} //namespace

int main(int argc, const char** argv) try {
    maps::cmdline::Parser parser("Add faked features to DB");

    maps::cmdline::Option<std::string> inputJsonPath = parser.string("input")
        .required()
        .help("Path to input json file with fearures");

    maps::cmdline::Option<std::string> mrcConfigPath = parser.string("mrc-config")
        .help("Path to mrc config");

    maps::cmdline::Option<std::string> secretVersion = parser.string("secret-version")
            .help("version for secrets from yav.yandex-team.ru");

    maps::cmdline::Option<bool> dryRunParam = parser.flag("dry-run")
        .help("do not save changes to database");

    parser.parse(argc, const_cast<char**>(argv));

    const maps::mrc::common::Config mrcConfig =
        maps::mrc::common::templateConfigFromCmdPath(secretVersion, mrcConfigPath);

    FeatureWithImages featureWithImages = loadFeaturesFromJson(inputJsonPath);
    INFO() << "Load " << featureWithImages.features.size() << " features from json";

    if (dryRunParam)
        return EXIT_SUCCESS;

    maps::wiki::common::PoolHolder mrc(mrcConfig.makePoolHolder());
    maps::mds::Mds mds = mrcConfig.makeMdsClient();
    uploadFeatures(mrc.pool(), mds, featureWithImages.features, featureWithImages.images);
    return EXIT_SUCCESS;
}
catch (const maps::Exception& e) {
    FATAL() << "Worker failed: " << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    FATAL() << "Worker failed: " << e.what();
    return EXIT_FAILURE;
}
