#include <maps/wikimap/mapspro/services/tasks_feedback/src/import_indoor_feedback_worker/lib/load_from_yt.h>

#include <library/cpp/yson/node/node.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/common/include/exception.h>

#include <algorithm>

namespace maps::wiki::tasks_feedback::indoor_feedback {

namespace {

std::string getId(const NYT::TNode& node)
{
    REQUIRE(node.HasValue(), "id has no value");
    return node.UncheckedAsString();
}

uint64_t getTimestamp(const NYT::TNode& node)
{
    REQUIRE(node.HasValue(), "timestamp has no value");
    return node.AsUint64();
}

std::string getIndoorLevel(const NYT::TNode& node)
{
    if (!node.HasValue()) {
        WARN() << "level has no value";
        return "";
    }
    return node.UncheckedAsString();
}

std::string getDescription(const NYT::TNode& node)
{
    if (!node.HasValue()) {
        WARN() << "title has no value";
        return "";
    }
    return node.UncheckedAsString();
}

Wall::Color getColor(const NYT::TNode& node)
{
    REQUIRE(node.HasValue(), "missing color");
    return enum_io::fromString<Wall::Color>(node.AsString());
}

geolib3::Point2 getPoint(const NYT::TNode& node)
{
    REQUIRE(
        node.HasValue() &&
        node.IsList() &&
        node.Size() == 2 &&
        node[0].HasValue() && node[0].IsDouble() &&
        node[1].HasValue() && node[1].IsDouble(),
        "Unexpecteted point format");
    return {node[0].AsDouble(), node[1].AsDouble()};
}

std::vector<geolib3::Point2> getPoints(const NYT::TNode& node)
{
    REQUIRE(
        node.HasValue() && node.IsList() && node.Size() > 0,
        "Unexpected coordinates format");
    std::vector<geolib3::Point2> points;
    for (const auto& point_node: node.AsList()) {
        points.push_back(getPoint(point_node));
    }
    return points;
}

bool hasStringKey(const NYT::TNode& node, const TString& key)
{
    return node.HasKey(key) &&
        node[key].IsString() &&
        node[key].HasValue();
}

bool isPoi(const NYT::TNode& row)
{
    return hasStringKey(row, "type") &&
        row["type"].AsString() == "Point";
}

bool isWall(const NYT::TNode& row)
{
    return hasStringKey(row, "type") &&
        row["type"].AsString() == "LineString";
}

Wall readWall(const NYT::TNode& row)
{
    return Wall{
            getTimestamp(row["timestamp"]),
            getIndoorLevel(row["level"]),
            getPoints(row["coordinates"]),
            getColor(row["color"])
        };
}

Poi readPoi(const NYT::TNode& row)
{
    return Poi{
        getIndoorLevel(row["level"]),
        getPoint(row["coordinates"]),
        getDescription(row["title"])
    };
}

bool isWithinTimeInterval(
    const TimeIntervalMs& timeIntervalMs,
    const NYT::TNode& row)
{
    const auto timestamp = getTimestamp(row["timestamp"]);
    return timeIntervalMs.fromTime <= timestamp &&
        timestamp < timeIntervalMs.tillTime;
}

} // namespace

YTData parseYTRows(const NYT::TNode::TListType& rows)
{
    YTData result;
    for (const auto& row : rows) {
        std::string rowId;
        try {
            rowId = getId(row["id"]);
            result.maxTimeStamp = std::max(
                result.maxTimeStamp,
                getTimestamp(row["timestamp"])
            );
            if (isPoi(row)) {
                auto poi = readPoi(row);
                result.pois.emplace_back(std::move(poi));
            } else if (isWall(row)) {
                auto wall = readWall(row);
                result.walls.emplace_back(std::move(wall));
            } else {
                REQUIRE(false, "Unknown row type");
            }
        } catch(maps::Exception& ex) {
            ERROR() << "Error processing row id '" << rowId << "': " << ex;
            throw;
        } catch(std::exception& ex) {
            ERROR() << "Error processing row id '" << rowId << "': " << ex.what();
            throw;
        }
    }
    return result;
}


YTData
loadData(const NYT::IClientPtr& iClientPtr,
         const TString& dynamicTablePath,
         const TimeIntervalMs& timeIntervalMs)
{
    REQUIRE(iClientPtr, "YT client is null");
    REQUIRE(
        timeIntervalMs.fromTime <= timeIntervalMs.tillTime,
        "Wrong time interval");

    auto clientTxn = iClientPtr->StartTransaction();
    auto reader = clientTxn->CreateTableReader<NYT::TNode>(dynamicTablePath);

    NYT::TNode::TListType rows;
    for (; reader->IsValid(); reader->Next()) {
        const NYT::TNode& row = reader->GetRow();
        if (isWithinTimeInterval(timeIntervalMs, row)) {
            rows.push_back(row);
        }
    }

    return parseYTRows(rows);
}

} // namespace maps::wiki::tasks_feedback::indoor_feedback
