#include <maps/wikimap/mapspro/services/autocart/pipeline/libs/factory/include/release.h>
#include <maps/wikimap/mapspro/services/autocart/libs/geometry/include/hex_wkb.h>
#include <maps/wikimap/mapspro/services/autocart/libs/geometry/include/polygon_processing.h>

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

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

#include <maps/libs/geolib/include/test_tools/comparison.h>


#include <memory>

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

namespace {

constexpr const char* FIELD_RELEASE_ID = "release_id";
constexpr const char* FIELD_ISSUE_ID = "issue_id";
constexpr const char* FIELD_ZMIN = "zmin";
constexpr const char* FIELD_ZMAX = "zmax";
constexpr const char* FIELD_SHAPE = "shape";
constexpr const char* FIELD_Z = "z";


} // namespace


NYT::TNode Release::toYTNode() const {
    NYT::TNode node;
    toYTNode(node);
    return node;
}

void Release::toYTNode(NYT::TNode& node) const {
    node[FIELD_ISSUE_ID] = issueId;
    node[FIELD_RELEASE_ID] = releaseId;
}

Release Release::fromYTNode(const NYT::TNode& node) {
    Release release;
    release.issueId = node[FIELD_ISSUE_ID].AsUint64();
    release.releaseId = node[FIELD_RELEASE_ID].AsUint64();
    return release;
}


NYT::TNode ReleaseGeometry::toYTNode() const {
    NYT::TNode node;
    toYTNode(node);
    return node;
}

void ReleaseGeometry::toYTNode(NYT::TNode& node) const {
    node[FIELD_ZMIN] = zmin;
    node[FIELD_ZMAX] = zmax;
    // Release geometries are stored in YT in mercator coordinates
    node[FIELD_SHAPE] = TString(multiPolygonToHexWKB(mercatorGeom));
}

ReleaseGeometry ReleaseGeometry::fromYTNode(const NYT::TNode& node) {
    ReleaseGeometry geometry;
    geometry.zmin = node[FIELD_ZMIN].AsInt64();
    geometry.zmax = node[FIELD_ZMAX].AsInt64();
    // Release geometries are stored in YT in mercator coordinates
    geometry.mercatorGeom = hexWKBToMultiPolygon(node[FIELD_SHAPE].AsString());
    return geometry;
}


bool Release::operator<(const Release& that) const {
    return issueId < that.issueId;
}

bool Release::operator==(const Release& that) const {
    return issueId == that.issueId && releaseId == that.releaseId;
}

std::ostream& operator<<(std::ostream& os, const Release& release) {
    os << "release id - " << release.releaseId
       << ", issue id - " << release.issueId;
    return os;
}


void ReleasesCoverage::toYTNode(NYT::TNode& node) const {
    node[FIELD_Z] = z;
    lastRelease.toYTNode(node);
    node[FIELD_SHAPE] = TString(multiPolygonToHexWKB(mercatorGeom));
}

NYT::TNode ReleasesCoverage::toYTNode() const {
    NYT::TNode node;
    toYTNode(node);
    return node;
}

ReleasesCoverage ReleasesCoverage::fromYTNode(const NYT::TNode& node) {
    ReleasesCoverage coverage;
    coverage.z = node[FIELD_Z].AsInt64();
    coverage.lastRelease = Release::fromYTNode(node);
    std::string hexWKB = node[FIELD_SHAPE].AsString();
    coverage.mercatorGeom = hexWKBToMultiPolygon(hexWKB);
    return coverage;
}



std::optional<Release> getReleaseByIssueId(FactoryServiceClient& client, uint64_t issueId) {
    std::set<Release> releases = client.getAllReleases();
    auto it = std::find_if(
        releases.begin(), releases.end(),
        [&](const Release& release) {
            return release.issueId == issueId;
        }
    );
    if (it != releases.end()) {
        return *it;
    } else {
        return std::nullopt;
    }
}

std::set<Release> getAllNextReleases(FactoryServiceClient& client, uint64_t releaseId) {
    std::set<Release> releases = client.getAllReleases();
    auto it = std::find_if(
        releases.begin(), releases.end(),
        [&](const Release& release) {
            return release.releaseId == releaseId;
        }
    );
    if (it != releases.end()) {
        return {std::next(it), releases.end()};
    } else {
        return {};
    }
}

bool ReleaseGeometry::operator==(const ReleaseGeometry& that) const {
    return zmin == that.zmin
        && zmax == that.zmax
        && geolib3::test_tools::approximateEqual(
            mercatorGeom, that.mercatorGeom, geolib3::EPS
        );
}


std::map<Release, ReleaseGeometries> loadAllNextReleasesGeometries(
    FactoryServiceClient& client, uint64_t releaseId)
{
    std::map<Release, ReleaseGeometries> releaseToGeometries;
    for (const Release& release: getAllNextReleases(client, releaseId)) {
        releaseToGeometries[release] =
            client.loadReleaseGeometries(release.releaseId);
    }
    return releaseToGeometries;
}

std::map<Release, ReleaseGeometries> loadAllReleasesGeometries(FactoryServiceClient& client) {
    std::map<Release, ReleaseGeometries> releaseToGeometries;
    for (const Release& release : client.getAllReleases()) {
        releaseToGeometries[release] = client.loadReleaseGeometries(release.releaseId);
    }
    return releaseToGeometries;
}

geolib3::MultiPolygon2 getReleasesCoverageAtZoom(
    const std::map<Release, ReleaseAttrs>& releaseToAttrs,
    int zoom)
{
    std::vector<geos::geom::Geometry*> ptrs;
    for (const auto& [release, attrs] : releaseToAttrs) {
        for (const ReleaseGeometry& geometry : attrs.geometries) {
            if (geometry.zmin <= zoom && zoom <= geometry.zmax) {
                // return shared_ptr that is contained in geolib3::MultiPolygon2
                std::shared_ptr<const geos::geom::MultiPolygon> geosPtr
                    = geolib3::internal::geolib2geosGeometry(geometry.mercatorGeom);
                ptrs.push_back(
                    const_cast<geos::geom::Geometry*>(
                        static_cast<const geos::geom::Geometry*>(geosPtr.get())
                    )
                );
            }
        }
    }
    std::unique_ptr<geos::geom::Geometry> united(
        geos::operation::geounion::CascadedUnion::Union(&ptrs)
    );
    return geolib3::MultiPolygon2(extractPolygons(*united));
}

std::pair<ReleaseToAttrs, ReleaseToAttrs> splitNotReadyReleases(
    const ReleaseToAttrs& releaseToAttrs,
    long int hoursGap)
{
    ReleaseToAttrs newReleaseToAttrs;
    ReleaseToAttrs notReadyReleaseToAttrs;

    chrono::TimePoint now = chrono::TimePoint::clock::now();
    for (const auto& [release, attrs] : releaseToAttrs) {
        if (std::chrono::duration_cast<std::chrono::hours>(now - attrs.date).count() < hoursGap) {
            notReadyReleaseToAttrs.emplace(release, attrs);
        } else {
            newReleaseToAttrs.emplace(release, attrs);
        }
    }
    return {newReleaseToAttrs, notReadyReleaseToAttrs};
}

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