#include <maps/libs/log8/include/log8.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/cmdline/include/cmdline.h>

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

#include <yandex/maps/wiki/common/robot.h>

#include <maps/wikimap/mapspro/libs/editor_client/include/instance.h>

#include <vector>
#include <thread>
#include <chrono>

using namespace maps;
using namespace maps::wiki;

namespace {

constexpr const char* BLD_ID = "bld_id";
constexpr const char* HEIGHT = "height";
constexpr const char* FT_TYPE_ID = "ft_type_id";

struct RemoveBuildingParams {
    uint64_t bldId;
};

struct UpdateBuildingParams {
    uint64_t bldId;
    std::unordered_map<std::string, std::string> plainAttributes;
};

// Input json format:
//   [
//     {"bld_id": 123, ...},
//     {"bld_id": 234, ...}
//   ]
//
std::vector<RemoveBuildingParams>
loadBuildingsForRemove(const std::string& jsonPath) {
    json::Value inputJson = json::Value::fromFile(jsonPath);
    std::vector<RemoveBuildingParams> paramsVector;
    paramsVector.reserve(inputJson.size());
    for (const json::Value& bldJson : inputJson) {
        RemoveBuildingParams params;
        params.bldId = bldJson[BLD_ID].as<uint64_t>();
        paramsVector.push_back(params);
    }
    return paramsVector;
}

void removeBuilding(
    const RemoveBuildingParams& params,
    editor_client::BasicEditorObject /* move */ /*object*/, // for common interface
    editor_client::Instance& instance)
{
    try {
        instance.deleteObject(params.bldId);
        INFO() << "Object with id = " << params.bldId << " removed";
    } catch (const maps::Exception& e) {
        ERROR() << e.what();
        throw maps::RuntimeError(
            "Failed to delete object with id = " + std::to_string(params.bldId));
    }
}

// Input json format:
//   [
//     {"bld_id": 123, "ft_type_id": 106},
//     {"bld_id": 234, "ft_type_id": 102, "height": 9}
//   ]
//
std::vector<UpdateBuildingParams> loadUpdatesForBuildings(const std::string& jsonPath) {
    static const std::string HEIGHT_ATTR = "bld:height";
    // see YMapsDF docs for bld:ft_type_id
    // https://doc.yandex-team.ru/ymaps/ymapsdf/ymapsdf-ref/concepts/bld.html
    static const std::string FT_TYPE_ID_ATTR = "bld:ft_type_id";

    json::Value inputJson = json::Value::fromFile(jsonPath);
    std::vector<UpdateBuildingParams> paramsVector;
    paramsVector.reserve(inputJson.size());
    for (const json::Value& bldJson : inputJson) {
        UpdateBuildingParams params;
        params.bldId = bldJson[BLD_ID].as<uint64_t>();
        if (bldJson.hasField(FT_TYPE_ID)) {
            params.plainAttributes[FT_TYPE_ID_ATTR]
                = std::to_string(bldJson[FT_TYPE_ID].as<uint64_t>());
        }
        if (bldJson.hasField(HEIGHT)) {
            params.plainAttributes[HEIGHT_ATTR]
                = std::to_string(bldJson[HEIGHT].as<uint64_t>());
        }
        if (!params.plainAttributes.empty()) {
            paramsVector.push_back(params);
        } else {
            INFO() << "There is no update for building with id = " << params.bldId;
        }
    }
    return paramsVector;
}

void updateBuilding(
    const UpdateBuildingParams& params,
    editor_client::BasicEditorObject /* move */ object,
    editor_client::Instance& instance)
{
    bool needUpdate = false;
    for (const auto& [name, value] : params.plainAttributes) {
        auto attrIt = object.plainAttributes.find(name);
        if (attrIt == object.plainAttributes.end()) {
            object.plainAttributes[name] = value;
            needUpdate = true;
        } else if (attrIt->second != value) {
            attrIt->second = value;
            needUpdate = true;
        }
    }
    if (needUpdate) {
        try {
            instance.saveObject(object);
            INFO() << "Building with bld_id " << params.bldId << " was updated";
        } catch (const maps::Exception& e) {
            ERROR() << e.what();
            throw maps::RuntimeError(
                "Failed to update object with id = " + std::to_string(params.bldId));
        }
    } else {
        INFO() << "Object with id = " << params.bldId << " has been already updated";
    }
}

void sleepMs(size_t delayMs) {
    if (delayMs > 0) {
        std::this_thread::sleep_for(std::chrono::milliseconds(delayMs));
    }
}

struct ChangeParams {
    std::string bldsJsonPath;
    std::string backendURL;
    uint64_t uid;
    size_t delayMs;
};

template <typename LoadChangesFunc, typename ChangeFunc>
void changeBuildings(
    const ChangeParams& changeParams,
    LoadChangesFunc&& loadChanges,
    ChangeFunc&& applyChange)
{
    INFO() << "Loading changes";
    auto changes = loadChanges(changeParams.bldsJsonPath);
    INFO() << "Loaded " << changes.size() << " changes";
    INFO() << "Create NMaps instance";
    INFO() << "  backend URL: " << changeParams.backendURL;
    INFO() << "  uid: " << changeParams.uid;
    editor_client::Instance instance(changeParams.backendURL, changeParams.uid);
    INFO() << "Applying changes";
    for (const auto& change : changes) {
        editor_client::BasicEditorObject object;
        try {
            object = instance.getObjectById(change.bldId);
        } catch (const maps::Exception& e) {
            ERROR() << e.what();
            throw maps::RuntimeError(
                "Failed to get object with id = " + std::to_string(change.bldId));
        }
        if (object.deleted) {
            INFO() << "Object with id = " << change.bldId << " has already been removed";
        } else {
            applyChange(change, std::move(object), instance);
        }
        sleepMs(changeParams.delayMs);
    }
}

using ActionFunc =
    std::function<
        void (const ChangeParams& params)>;

const std::unordered_map<std::string, ActionFunc>
    ACTION_TYPE_TO_FUNC = {
        {
            "remove",
            [](const ChangeParams& params) {
                changeBuildings(params, loadBuildingsForRemove, removeBuilding);
            }
        },
        {
            "update",
            [](const ChangeParams& params) {
                changeBuildings(params, loadUpdatesForBuildings, updateBuilding);
            }
        }
    };

std::string getActionHelpStr() {
    std::stringstream ss;
    ss << "Action type:\n";
    size_t i = 1;
    for (const auto& [actionType, func] : ACTION_TYPE_TO_FUNC) {
        ss << "  " << i << ") " << actionType << "\n";
        i++;
    }
    return ss.str();
}

const std::unordered_map<std::string, std::string> BACKEND_TYPE_TO_URL {
    {"testing", "http://core-nmaps-editor.common.testing.maps.yandex.net"},
    {"production", "http://core-nmaps-editor-writer.maps.yandex.net"}
};

std::string getBackendHelpStr() {
    std::stringstream ss;
    ss << "Backend type:\n";
    size_t i = 1;
    for (const auto& [backendType, url] : BACKEND_TYPE_TO_URL) {
        ss << "  " << i << ") " << backendType << " - " << url << "\n";
        i++;
    }
    return ss.str();
}

std::string getUIDHelpStr() {
    std::stringstream ss;
    ss << "User id. Default user - \"wikimaps-bld\" : " << maps::wiki::common::WIKIMAPS_BLD_UID;
    return ss.str();
}

} // namespace

int main(int argc, const char** argv)
try {
    maps::cmdline::Parser parser("Change buildings in NMaps");

    maps::cmdline::Option<std::string> bldsJsonPath = parser.string("blds_json")
        .required()
        .help("Path to json file with buildings");

    maps::cmdline::Option<std::string> actionType = parser.string("action")
        .required()
        .help(getActionHelpStr());

    maps::cmdline::Option<std::string> backendType = parser.string("backend")
        .required()
        .help(getBackendHelpStr());

    maps::cmdline::Option<size_t> uid = parser.size_t("uid")
        .defaultValue(maps::wiki::common::WIKIMAPS_BLD_UID)
        .help(getUIDHelpStr());

    maps::cmdline::Option<size_t> delayMs = parser.size_t("delay")
        .defaultValue(0)
        .help("Delay in changing buildings (milliseconds)");

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

    REQUIRE(ACTION_TYPE_TO_FUNC.count(actionType), "Unknown action: " << actionType);

    REQUIRE(BACKEND_TYPE_TO_URL.count(backendType), "Invalid backend type: " << backendType);

    ChangeParams params;
    params.bldsJsonPath = bldsJsonPath;
    params.backendURL = BACKEND_TYPE_TO_URL.at(backendType);
    params.uid = uid;
    params.delayMs = delayMs;
    ACTION_TYPE_TO_FUNC.at(actionType)(params);

    return EXIT_SUCCESS;
}
catch (const maps::Exception& e) {
    ERROR() << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    ERROR() << e.what();
    return EXIT_FAILURE;
}
catch (...) {
    ERROR() << "unknown error";
    return EXIT_FAILURE;
}
