#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 <maps/libs/json/include/builder.h>

#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/storage/include/publication_results.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/storage/include/yt_storage.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/storage/include/strings.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/detection/include/compare_blds.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/yt_utils/include/op_wrapper.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/objects/include/building.h>

#include <mapreduce/yt/util/temp_table.h>
#include <mapreduce/yt/util/ypath_join.h>
#include <mapreduce/yt/interface/client.h>

#include <set>
#include <string>
#include <fstream>
#include <unordered_map>

using namespace maps::wiki::autocart::pipeline;

namespace json = maps::json;

namespace {

constexpr const char* SHAPE = "shape";

std::set<std::pair<TString, uint64_t>> readRegionAndIssueIdSet(const std::string& path) {
    std::ifstream ifs(path);
    REQUIRE(ifs.is_open(), "Failed to open file: " + path);

    std::set<std::pair<TString, uint64_t>> regionAndIssueIdSet;
    while (!ifs.eof()) {
        std::string line;
        std::getline(ifs, line);

        if (line.empty()) {
            continue;
        }

        size_t commaPos = line.find(',');
        REQUIRE(commaPos != std::string::npos, "Invalid line: " + line);
        TString region = TString(line.substr(0, commaPos));
        uint64_t issueId = std::stoull(line.substr(commaPos + 1));
        regionAndIssueIdSet.insert(std::make_pair(region, issueId));
    }

    return regionAndIssueIdSet;
}

class ExtractPublishedMapper
    : public NYT::IMapper<NYT::TTableReader<NYT::TNode>,
                          NYT::TTableWriter<NYT::TNode>> {
public:
    ExtractPublishedMapper() = default;
    ExtractPublishedMapper(
        const std::set<std::pair<TString, uint64_t>>& regionAndIssueIdSet)
        : regionAndIssueIdSet_(regionAndIssueIdSet)
    {}

    Y_SAVELOAD_JOB(regionAndIssueIdSet_);

    void Do(NYT::TTableReader<NYT::TNode>* reader,
            NYT::TTableWriter<NYT::TNode>* writer) override {
        for (; reader->IsValid(); reader->Next()) {
            const NYT::TNode& row = reader->GetRow();
            if (!regionAndIssueIdSet_.empty()) {
                std::pair<TString, uint64_t> regionAndIssueId
                    = std::make_pair(row[REGION].AsString(), row[ISSUE_ID].AsUint64());
                if (regionAndIssueIdSet_.count(regionAndIssueId) == 0) {
                    continue;
                }
            }
            PublicationResult result = PublicationResult::fromYTNode(row);
            if (result.status == PublicationStatus::Published) {
                writer->AddRow(row);
            }
        }
    }

private:
    std::set<std::pair<TString, uint64_t>> regionAndIssueIdSet_;
};

REGISTER_MAPPER(ExtractPublishedMapper);

void mergeBuildingsFromStorage(
    NYT::ITransactionPtr txn,
    YTStorageClient& storage,
    const std::set<std::pair<TString, uint64_t>>& regionAndIssueIdSet,
    const NYT::TTempTable& outputTable)
{
    NYT::TTempTable publicationTable(txn);
    storage.cloneResultsTable<PublicationResult>(
        txn,
        publicationTable.Name()
    );

    YTOpExecutor::Map(
        txn,
        YTOpExecutor::MapSpec()
            .AddInput(publicationTable.Name())
            .AddOutput(outputTable.Name()),
        new ExtractPublishedMapper(regionAndIssueIdSet)
    );
    txn->Sort(
        NYT::TSortOperationSpec()
            .AddInput(outputTable.Name())
            .Output(outputTable.Name())
            .SortBy({Building::BLD_ID})
    );
}

class MergeYMapsDFReducer
    : public NYT::IReducer<NYT::TTableReader<NYT::TNode>,
                           NYT::TTableWriter<NYT::TNode>> {
    void Do(NYT::TTableReader<NYT::TNode>* reader,
            NYT::TTableWriter<NYT::TNode>* writer) override {
        NYT::TNode node;

        for (; reader->IsValid(); reader->Next()) {
            const NYT::TNode row = reader->GetRow();
            for (const auto& [name, value] : row.AsMap()) {
                node[name] = value;
            }
        }
        if (!node[SHAPE].IsUndefined()) { // bld is not deleted
            writer->AddRow(node);
        }
    }
};

REGISTER_REDUCER(MergeYMapsDFReducer);

void mergeBuildingsFromYMapsDF(
    NYT::ITransactionPtr txn,
    const TString& ymapsdfYTPath,
    const NYT::TTempTable& outputTable)
{
    static TString BLD_TABLE = "bld";
    static TString BLD_GEOM_TABLE = "bld_geom";

    YTOpExecutor::ReduceSpec spec;
    NYT::TNode::TListType regionsListNode = txn->List(ymapsdfYTPath);
    for (int i = 0; i < regionsListNode.ysize(); i++) {
        TString regionName = regionsListNode[i].AsString();
        spec.AddInput(NYT::JoinYPaths(ymapsdfYTPath, regionName, BLD_TABLE));
        spec.AddInput(NYT::JoinYPaths(ymapsdfYTPath, regionName, BLD_GEOM_TABLE));
    }
    spec.AddOutput(outputTable.Name());
    spec.ReduceBy({Building::BLD_ID});
    YTOpExecutor::Reduce(txn, spec, new MergeYMapsDFReducer());
    txn->Sort(
        NYT::TSortOperationSpec()
            .AddInput(outputTable.Name())
            .Output(outputTable.Name())
            .SortBy({Building::BLD_ID})
    );
}

} // namespace

int main(int argc, const char** argv)
try {
    NYT::Initialize(argc, argv);

    maps::cmdline::Parser parser("Filter geometries by regions");

    maps::cmdline::Option<std::string> storagePath = parser.string("storage_path")
        .required()
        .help("Path to YT storage");

    maps::cmdline::Option<std::string> ymapsdfYTPath = parser.string("ymapsdf_path")
        .defaultValue("//home/maps/core/garden/stable/ymapsdf/latest")
        .help("Path to YT folder with YMapsDF");

    maps::cmdline::Option<std::string> regionAndIssueIdSetPath = parser.string("region_and_issue_id")
        .help("Path to txt file with regions and issue ids");

    maps::cmdline::Option<std::string> outputJsonPath = parser.string("output")
        .required()
        .help("Path to output json file with results of comparision");

    maps::cmdline::Option<double> iouThreshold = parser.real("iou")
        .required()
        .help("Threshold for IoU");

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

    INFO() << "Connecting to yt::hahn";
    NYT::IClientPtr client = NYT::CreateClient("hahn");

    INFO() << "Connecting to YT storage: " << storagePath;
    YTStorageClient storage(client, TString(storagePath));

    NYT::ITransactionPtr txn = client->StartTransaction();

    INFO() << "Creating gt table with buildings";
    std::set<std::pair<TString, uint64_t>> regionAndIssueIdSet;
    if (regionAndIssueIdSetPath.defined()) {
        regionAndIssueIdSet = readRegionAndIssueIdSet(regionAndIssueIdSetPath);
    }
    NYT::TTempTable gtTable(txn);
    mergeBuildingsFromStorage(txn, storage, regionAndIssueIdSet, gtTable);

    INFO() << "Creating test table with buildings";
    NYT::TTempTable testTable(txn);
    mergeBuildingsFromYMapsDF(txn, TString(ymapsdfYTPath), testTable);

    INFO() << "Comparing buildings";
    std::vector<Change> changes = compareBuildings(
        txn,
        gtTable.Name(), testTable.Name(),
        iouThreshold
    );

    txn->Commit();

    std::ofstream ofs(outputJsonPath);
    REQUIRE(ofs.is_open(), "Failed to open file: " + outputJsonPath);
    json::Builder builder(ofs);
    builder << [&](json::ArrayBuilder b) {
        for (const Change& change : changes) {
            b << [&](json::ObjectBuilder b) {
                changeToJson(change, b);
            };
        }
    };
    ofs.close();

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