#include <maps/libs/geolib/include/polygon.h>

#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/objects/include/building.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/detection/include/compare_blds.h>
#include <maps/wikimap/mapspro/services/autocart/libs/geometry/include/polygon_processing.h>
#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/yt_utils/include/op_wrapper.h>

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

namespace maps::wiki::autocart::pipeline {

namespace {

constexpr const char* IS_DELETED = "is_deleted";
constexpr const char* IS_GEOM_CHANGED = "is_geom_changed";
constexpr const char* HEIGHT_CHANGE = "height_change";
constexpr const char* FT_TYPE_ID_CHANGE = "ft_type_id_change";

Change convertYTNodeToChange(const NYT::TNode& node) {
    Change change;
    change.bldId = node[Building::BLD_ID].AsUint64();
    change.isDeleted = node[IS_DELETED].AsBool();
    if (change.isDeleted) {
        return change;
    }
    NYT::TNode isGeomChangedNode = node[IS_GEOM_CHANGED];
    if (!isGeomChangedNode.IsUndefined() && !isGeomChangedNode.IsNull()) {
        change.isGeomChanged = node[IS_GEOM_CHANGED].AsBool();
    }
    NYT::TNode ftTypeIdChangeNode = node[FT_TYPE_ID_CHANGE];
    if (!ftTypeIdChangeNode.IsUndefined() && !ftTypeIdChangeNode.IsNull()) {
        NYT::TNode::TListType changeList = ftTypeIdChangeNode.AsList();
        FTTypeId gtFTTypeId = decodeFTTypeId(changeList[0].AsInt64());
        FTTypeId testFTTypeId = decodeFTTypeId(changeList[1].AsInt64());
        change.ftTypeIdChange = std::make_pair(gtFTTypeId, testFTTypeId);
    }
    NYT::TNode heightChangeNode = node[HEIGHT_CHANGE];
    if (!heightChangeNode.IsUndefined() && !heightChangeNode.IsNull()) {
        NYT::TNode::TListType changeList = heightChangeNode.AsList();
        int gtHeight = changeList[0].AsInt64();
        int testHeight = changeList[1].AsInt64();
        change.heightChange = std::make_pair(gtHeight, testHeight);
    }
    return change;
}

NYT::TNode convertChangeToYTNode(const Change& change) {
    NYT::TNode node;
    node[Building::BLD_ID] = change.bldId;
    if (change.isDeleted) {
        node[IS_DELETED] = true;
    } else {
        node[IS_DELETED] = false;
        node[IS_GEOM_CHANGED] = change.isGeomChanged;
        if (change.ftTypeIdChange) {
            node[FT_TYPE_ID_CHANGE]
                = NYT::TNode::CreateList()
                      .Add(encodeFTTypeId(change.ftTypeIdChange->first))
                      .Add(encodeFTTypeId(change.ftTypeIdChange->second));
        }
        if (change.heightChange) {
            node[HEIGHT_CHANGE]
                = NYT::TNode::CreateList()
                      .Add(change.heightChange->first)
                      .Add(change.heightChange->second);
        }
    }
    return node;
}

class CompareBuildingsReducer
    : public NYT::IReducer<NYT::TTableReader<NYT::TNode>,
                           NYT::TTableWriter<NYT::TNode>> {
public:
    CompareBuildingsReducer() = default;
    CompareBuildingsReducer(double iou)
        : iou_(iou)
    {}

    Y_SAVELOAD_JOB(iou_);

    void Do(NYT::TTableReader<NYT::TNode>* reader,
            NYT::TTableWriter<NYT::TNode>* writer) override {
        std::optional<Building> gtBld;
        std::optional<Building> testBld;
        for (; reader->IsValid(); reader->Next()) {
            const NYT::TNode& row = reader->GetRow();
            size_t tableIndex = reader->GetTableIndex();
            if (0 == tableIndex) {
                gtBld = Building::fromYTNode(row);
            } else if (1 == tableIndex){
                testBld = Building::fromYTNode(row);
            } else {
                Y_FAIL();
            }
        }
        if (!gtBld) {
            return; // building is not published by robot
        }
        Change change;
        if (!isEqualBuildings(*gtBld, testBld, change)) {
            writer->AddRow(convertChangeToYTNode(change));
        }
    }

private:
    bool isEqualBuildings(
       const Building& gtBld,
       const std::optional<Building>& testBld,
       Change& change) const
    {
        change.bldId = gtBld.getId();
        if (!testBld) {
            change.isDeleted = true;
            return false;
        } else {
            change.isDeleted = false;
        }
        change.isGeomChanged
            = (IoU(gtBld.toGeodeticGeom(), testBld->toGeodeticGeom()) < iou_);
        if (gtBld.getFTTypeId() != testBld->getFTTypeId()) {
            change.ftTypeIdChange
                = std::make_pair(gtBld.getFTTypeId(), testBld->getFTTypeId());
        }
        if (!gtBld.hasHeight() && testBld->hasHeight()) {
            change.heightChange = std::make_pair(NULL_HEIGHT, testBld->getHeight());
        } else if (gtBld.hasHeight() && !testBld->hasHeight()) {
            change.heightChange = std::make_pair(gtBld.getHeight(), NULL_HEIGHT);
        } else if (gtBld.hasHeight() && testBld->hasHeight()) {
            if (gtBld.getHeight() != testBld->getHeight()) {
                change.heightChange
                    = std::make_pair(gtBld.getHeight(), testBld->getHeight());
            }
        }
        return !change.isGeomChanged && !change.ftTypeIdChange && !change.heightChange;
    }

    double iou_;
};

REGISTER_REDUCER(CompareBuildingsReducer);

} // namespace

void changeToJson(const Change& change, json::ObjectBuilder& builder) {
    builder[Building::BLD_ID] = change.bldId;
    if (change.isDeleted) {
        builder[IS_DELETED] = true;
    } else {
        builder[IS_DELETED] = false;
        builder[IS_GEOM_CHANGED] = change.isGeomChanged;
        if (change.heightChange.has_value()) {
            builder[HEIGHT_CHANGE] = [&](json::ArrayBuilder b) {
                if (change.heightChange->first != NULL_HEIGHT) {
                    b << change.heightChange->first;
                } else {
                    b << json::null;
                }
                if (change.heightChange->second != NULL_HEIGHT) {
                    b << change.heightChange->second;
                } else {
                    b << json::null;
                }
            };
        } else {
            builder[HEIGHT_CHANGE] = json::null;
        }
        if (change.ftTypeIdChange.has_value()) {
            builder[FT_TYPE_ID_CHANGE] = [&](json::ArrayBuilder b) {
                b << encodeFTTypeId(change.ftTypeIdChange->first);
                b << encodeFTTypeId(change.ftTypeIdChange->second);
            };
        } else {
            builder[FT_TYPE_ID_CHANGE] = json::null;
        }
    }
}

std::vector<Change> compareBuildings(
    NYT::IClientBasePtr client,
    const TString& gtTablePath,
    const TString& testTablePath,
    double iou)
{
    NYT::ITransactionPtr txn = client->StartTransaction();

    NYT::TTempTable compareTable(txn);
    YTOpExecutor::Reduce(
        txn,
        YTOpExecutor::ReduceSpec()
            .AddInput(gtTablePath)
            .AddInput(testTablePath)
            .AddOutput(compareTable.Name())
            .ReduceBy({Building::BLD_ID}),
        new CompareBuildingsReducer(iou)
    );

    std::vector<Change> changes;
    NYT::TTableReaderPtr<NYT::TNode> reader
        = txn->CreateTableReader<NYT::TNode>(compareTable.Name());
    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode row = reader->GetRow();
        changes.push_back(convertYTNodeToChange(row));
    }

    return changes;
}

} // namespace maps::wiki::autocart::pipeline
