#include "import.h"
#include "common.h"
#include "dataset.h"
#include "geometry.h"
#include "maps/libs/geolib/include/heading.h"
#include "maps/libs/geolib/include/point.h"
#include "maps/wikimap/mapspro/services/mrc/long_tasks/import_nexar/lib/nexar_client.h"
#include "nexar_import_tile_update_info.h"
#include "suncalc.h"

#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/for_each_batch.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/geometry.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/opencv.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/algorithm/parallel_for_each.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/mds_path.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/types.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/import_config_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/nexar_frame_to_feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/visibility.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/export_gen/lib/graph.h>

#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/direction.h>
#include <maps/libs/geolib/include/intersection.h>
#include <maps/libs/geolib/include/projection.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>
#include <maps/libs/geolib/include/test_tools/io_operations.h>
#include <maps/libs/introspection/include/comparison.h>
#include <maps/libs/introspection/include/stream_output.h>
#include <maps/libs/tile/include/geometry.h>
#include <maps/libs/log8/include/log8.h>

#include <algorithm>
#include <chrono>
#include <mutex>
#include <set>

namespace maps::geolib3 {
using maps::geolib3::io::operator<<;
} // namespace maps::geolib3

namespace maps::mrc::import_nexar {

using introspection::operator==;
using introspection::operator<;
using maps::introspection::operator<<;

namespace {

constexpr float FRONT_VISIBILITY_METERS = 10;
constexpr float SIDE_VISIBILITY_METERS = 25;
constexpr int NEXAR_CLIENT_CONCURRENCY = 10;
/// Consider that night photo is worth as 30-days old daylight photo
constexpr std::chrono::days NIGHT_PHOTOS_PENALTY{30};
using export_gen::COVERAGE_ANGLE_DIFF_THRESHOLD ;

std::vector<NexarImageMeta> makeUnique(std::vector<std::vector<NexarImageMeta>>&& imagesVec)
{
    std::set<FrameId> addedImageFrameIds;
    std::vector<NexarImageMeta> uniqueImages;

    for (auto& images : imagesVec) {
        for (auto image : images) {
            if (!addedImageFrameIds.count(image.frameId)) {
                addedImageFrameIds.insert(image.frameId);
                uniqueImages.push_back((std::move(image)));
            }
        }
    }
    return uniqueImages;
}

geolib3::Polygon2 narrowFieldOfView(const geolib3::Point2& pos, const geolib3::Direction2 direction)
{
    auto directionVec = direction.vector();
    auto a = geolib3::fastGeoShift(
        pos,
        SIDE_VISIBILITY_METERS *
            geolib3::rotateBy90(directionVec, geolib3::Counterclockwise));
    auto b = geolib3::fastGeoShift(a, directionVec * FRONT_VISIBILITY_METERS);
    auto c = geolib3::fastGeoShift(
        b,
        2 * SIDE_VISIBILITY_METERS *
            geolib3::rotateBy90(directionVec, geolib3::Clockwise));
    auto d = geolib3::fastGeoShift(
        pos,
        SIDE_VISIBILITY_METERS * geolib3::rotateBy90(directionVec, geolib3::Clockwise));
    return geolib3::Polygon2{geolib3::PointsVector{a, b, c, d}};
}

geolib3::PointsVector evalIntersectedPoints(
    const geolib3::Polygon2& convexPolygon,
    const geolib3::Segment2& segment)
{
    std::set<geolib3::Point2> points;
    auto ring = convexPolygon.exteriorRing();
    for (size_t i = 0; i < ring.segmentsNumber(); ++i) {
        for (auto point : geolib3::intersection(ring.segmentAt(i), segment)) {
            points.insert(point);
        }
    }
    return {points.begin(), points.end()};
}

std::optional<geolib3::Point2> getPointNotEqualTo(
    const geolib3::PointsVector& points,
    const geolib3::Point2& point)
{
    for (const auto& onePoint : points) {
        if (onePoint != point) {
            return onePoint;
        }
    }
    return std::nullopt;
}

std::optional<common::geometry::SubSegment>
evalSegmentCoverage(
    const geolib3::Segment2& mercatorSegment,
    geolib3::Direction2 direction,
    const geolib3::Polygon2& mercatorFieldOfView)
{
    const float MIN_REL_POS = 0.;
    const float MAX_REL_POS = 1.;

    auto angle =
        geolib3::angleBetween(direction, geolib3::Direction2(mercatorSegment));

    if (angle >= COVERAGE_ANGLE_DIFF_THRESHOLD) {
        return std::nullopt;
    }

    if (!geolib3::spatialRelation(
            mercatorFieldOfView, mercatorSegment, geolib3::Intersects)) {
        return std::nullopt;
    }

    bool intersectsStart = geolib3::spatialRelation(
        mercatorFieldOfView, mercatorSegment.start(), geolib3::Intersects);
    bool intersectsEnd = geolib3::spatialRelation(
        mercatorFieldOfView, mercatorSegment.end(), geolib3::Intersects);
    if (intersectsStart && intersectsEnd) {
        return common::geometry::SubSegment{MIN_REL_POS, MAX_REL_POS};
    }
    auto intersectionPoints =
        evalIntersectedPoints(mercatorFieldOfView, mercatorSegment);
    if (intersectionPoints.empty()) {
        return std::nullopt;
    }
    if (intersectsStart) {
        auto otherPoint =
            getPointNotEqualTo(intersectionPoints, mercatorSegment.start());
        if (!otherPoint.has_value()) {
            return std::nullopt;
        }
        return common::geometry::SubSegment{MIN_REL_POS,
                          static_cast<float>(geolib3::projectionFactor(
                              mercatorSegment, otherPoint.value()))};
    }
    else if (intersectsEnd) {
        auto otherPoint =
            getPointNotEqualTo(intersectionPoints, mercatorSegment.end());
        if (!otherPoint.has_value()) {
            return std::nullopt;
        }
        return common::geometry::SubSegment{static_cast<float>(geolib3::projectionFactor(
                              mercatorSegment, otherPoint.value())),
                          MAX_REL_POS};
    }
    else if (intersectionPoints.size() == 1) {
        return std::nullopt;
    }
    ASSERT(intersectionPoints.size() >= 2);
    auto relPosA = static_cast<float>(
        geolib3::projectionFactor(mercatorSegment, intersectionPoints.front()));
    auto relPosB = static_cast<float>(
        geolib3::projectionFactor(mercatorSegment, intersectionPoints.back()));
    return common::geometry::SubSegment{std::min(relPosA, relPosB), std::max(relPosA, relPosB)};
}

std::vector<FrameId> collectFrameIds(std::vector<NexarImageMeta>& images)
{
    std::vector<FrameId> frameIds;
    frameIds.reserve(images.size());
    for (const auto& image : images) {
        frameIds.push_back(image.frameId);
    }
    return frameIds;
}

std::set<FrameId> filterAlreadyImportedFrameIds(
    pqxx::transaction_base& txn, const std::vector<FrameId>& frameIds)
{
    std::set<FrameId> result;
    db::NexarFrameToFeatureGateway gtw(txn);
    common::forEachBatch(frameIds, 1000,
        [&](auto begin, auto end){
            std::vector<FrameId> framesBatch(begin, end);
            auto existingItems = gtw.load(db::table::NexarFrameToFeatureTable::frameId.in(framesBatch));
            for (const auto& frameToFeature : existingItems) {
                result.insert(frameToFeature.frameId());
            }
        });
    return result;
}

} // namespace

using introspection::operator==;
using introspection::operator<;

db::ImportConfigs loadImportConfigs(pqxx::transaction_base& txn)
{
    return db::ImportConfigGateway(txn).load(
        db::table::ImportConfig::dataset == db::Dataset::NexarDashcams);
}

std::vector<TileImportParams> evalTileImportParams(
    const privacy::GeoIdProvider& geoIdProvider,
    const db::ImportConfig& config,
    int32_t tilingZoom)
{
    std::vector<TileImportParams> result;
    auto geodeticGeom = geoIdProvider.geomById(config.geoId());
    auto mercatorGeom = geolib3::convertGeodeticToMercator(geodeticGeom);
    auto tilesSet =
        tile::intersectedTiles(mercatorGeom, tilingZoom, tilingZoom);
    for (const auto& tile: tilesSet) {
        result.push_back(
            {static_cast<int32_t>(config.geoId()),
             config.fc(),
             static_cast<int32_t>(tile.x()),
             static_cast<int32_t>(tile.y()),
             static_cast<int32_t>(tile.z())});
    }
    return result;
}

bool shouldUpdateTile(
    const db::ImportConfig& config,
    const db::NexarImportTileUpdateInfo& tileUpdateInfo,
    chrono::TimePoint checkTime)
{
    constexpr std::chrono::days MAX_RECHECK_PERIOD{30};

    if (!tileUpdateInfo.checkedAt().has_value()) {
        return true;
    }

    const std::chrono::days durationSinceLastCheck =
        std::chrono::duration_cast<std::chrono::days>(
            checkTime - tileUpdateInfo.checkedAt().value());

    auto durationThresold = MAX_RECHECK_PERIOD;

    if (tileUpdateInfo.medianPhotoAgeDays().has_value()) {
        /// The intuition is this: it is unlikely that new photos would appear
        /// earlier that after medianPhotoAgeDays after last check
        int32_t predictedDaysToRecheck = std::max(
            tileUpdateInfo.medianPhotoAgeDays().value(),
            static_cast<int32_t>(config.thresholdDays()));
        durationThresold = std::min(
            durationThresold, std::chrono::days(predictedDaysToRecheck));
    }

    if (durationSinceLastCheck > durationThresold) {
        return true;
    }
    return false;
}

std::vector<TileImportParams> evalTileImportParamsToCheck(
    const Metadata& metadata,
    const db::ImportConfig& config,
    std::vector<TileImportParams> params,
    chrono::TimePoint checkTime)
{
    std::vector<TileImportParams> result;
    result.reserve(params.size());

    for (auto& param: params) {
        auto tileUpdateInfo = metadata.getTileUpdateInfo(param);
        if (shouldUpdateTile(config, tileUpdateInfo, checkTime)) {
            result.push_back(std::move(param));
        }
    }
    return result;
}

std::vector<EdgeWithGeometry>
evalGeodeticPolylinesToImport(const road_graph::Graph& graph,
                      const succinct_rtree::Rtree& rtree,
                      const TileImportParams& param)
{
    std::vector<EdgeWithGeometry> result;
    const auto mercatorBbox = tile::mercatorBBox(tile::Tile(param.x, param.y, param.z));
    const auto geodeticBbox = geolib3::convertMercatorToGeodetic(mercatorBbox);

    for (auto edgeId : rtree.baseEdgesInWindow(geodeticBbox)) {
        auto edgeData = graph.edgeData(edgeId);
        if (edgeData.category() != static_cast<uint16_t>(param.fc)) {
            continue;
        }
        const geolib3::Polyline2 geodeticPolyline = edgeData.geometry();
        const auto mercatorPolyline = geolib3::convertGeodeticToMercator(geodeticPolyline);
        const auto polylinesInTile = geolib3::intersection(mercatorPolyline, mercatorBbox);
        for (const auto& polyline : polylinesInTile) {
            result.push_back(
                {edgeId, geolib3::convertMercatorToGeodetic(polyline)});
        }
    }
    return result;
}

common::geometry::SubPolylines
evalCoverage(const geolib3::Polyline2& geoPolyline, NexarImageMetaCRef image)
{
    const geolib3::Direction2 direction(image.get().heading);
    const auto fovPolygon = narrowFieldOfView(image.get().position, direction);
    const auto mercFovPolygon = geolib3::convertGeodeticToMercator(fovPolygon);
    const auto mercPolyline = geolib3::convertGeodeticToMercator(geoPolyline);

    common::geometry::SubPolylines subpolylines;
    for (size_t i = 0; i < mercPolyline.segmentsNumber(); ++i) {
        if (auto maybeCoverage = evalSegmentCoverage(
                mercPolyline.segmentAt(i), direction, mercFovPolygon)) {
            subpolylines.push_back(
                {{static_cast<uint16_t>(i), maybeCoverage->begin()},
                    {static_cast<uint16_t>(i), maybeCoverage->end()}});
        }
    }
    return common::geometry::merge(subpolylines);
}

std::vector<SubpolylineWithImageRef>
evalUniqueActualCoverage(const geolib3::Polyline2& polyline,
                         NexarImageMetaCRefs images)
{
    std::vector<SubpolylineWithImageRef> result;

    for (NexarImageMetaCRef image : images) {
        for(auto subpolyline : evalCoverage(polyline, image)) {
            result.emplace_back(subpolyline, image);
        }
    }

    auto lessSuitable =
        [](const auto& frame1, const auto& frame2) {
            if (frame1.get().dayPart == frame2.get().dayPart) {
                return frame1.get().capturedAt < frame2.get().capturedAt;
            } else {
                ASSERT(
                    frame1.get().dayPart.has_value() &&
                    frame2.get().dayPart.has_value());

                auto frame1TimeModifier =
                    frame1.get().dayPart.value() == common::DayPart::Day
                        ? NIGHT_PHOTOS_PENALTY
                        : -NIGHT_PHOTOS_PENALTY;
                return frame1.get().capturedAt + frame1TimeModifier
                    < frame2.get().capturedAt;
            }
        };

    return common::geometry::merge(result, lessSuitable);
}

std::vector<ImageWithCoverage> selectBestCoveringFrames(
    const road_graph::Graph& graph,
    const succinct_rtree::Rtree& rtree,
    const TileImportParams& param,
    const INexarClient& nexarClient)
{
    constexpr int H3_RESOLUTION = 12;
    constexpr size_t NEXAR_SEARCH_LIMIT = 50;

    std::set<NexarSpatialSearchParams> searchParams;
    std::vector<EdgeWithGeometry> edgesToImport =
        evalGeodeticPolylinesToImport(graph, rtree, param);
    for (const auto& edge : edgesToImport) {
        auto edgeSearchParams = evalSearchParams(edge.geometry, H3_RESOLUTION);
        searchParams.insert(edgeSearchParams.begin(), edgeSearchParams.end());
    }

    auto imagesVec = evalConcurently(
        [&](const NexarSpatialSearchParams& param) -> std::vector<NexarImageMeta>
        {
            auto images = nexarClient.search(param, std::nullopt, NEXAR_SEARCH_LIMIT);
            fillDayPart(images);
            return images;
        },
        NEXAR_CLIENT_CONCURRENCY,
        searchParams);

    auto uniqueImages = makeUnique(std::move(imagesVec));
    geolib3::StaticGeometrySearcher<geolib3::Point2, NexarImageMetaCRef>
        imagesSearcher;
    for(const auto& image : uniqueImages) {
        imagesSearcher.insert(&image.position, std::cref(image));
    }
    imagesSearcher.build();

    std::vector<ImageWithCoverage> result;

    for(const auto& edge : edgesToImport) {
        auto expandedEdgeBbox = db::addVisibilityMargins(edge.geometry.boundingBox());
        auto searchResult = imagesSearcher.find(expandedEdgeBbox);
        NexarImageMetaCRefs imagesRefs;
        for (auto it = searchResult.first; it != searchResult.second; it++) {
            imagesRefs.push_back(it->value());
        }

        auto edgeData = graph.edgeData(edge.edgeId);
        const geolib3::Polyline2 edgeFullGeodeticPolyline = edgeData.geometry();
        for (SubpolylineWithImageRef imageWithSubpolyline :
                 evalUniqueActualCoverage(edgeFullGeodeticPolyline, imagesRefs))
        {
            result.push_back(
                ImageWithCoverage {
                    .imageMeta = imageWithSubpolyline.value.get(),
                    .edgeId = edge.edgeId,
                    .subpolyline = imageWithSubpolyline.subPolyline
                }
            );
        }
    }

    return result;
}

bool improvesEdgeCoverage(
    const ImageWithCoverage& imageWithCoverage, const fb::TEdge& edge)
{
    // The image is worth importing if it is captured more than
    // FRESHNESS_THRESHOLD after the one we are using now in coverage
    const std::chrono::days FRESHNESS_THRESHOLD{10};

    if (imageWithCoverage.subpolyline.empty()) {
        return false;
    }

    common::geometry::SubPolylines intersectedSubpolylines;

    for (const auto& edgeCoverage : edge.coverages) {
        // Take into account only coverages in Front direction
        if (edgeCoverage.cameraDeviation != db::CameraDeviation::Front) {
            continue;
        }

        for (const auto& fbSubpolyline : edgeCoverage.coveredSubpolylines) {
            auto subpolyline = fb::convertToCommonSubpolyline(fbSubpolyline);
            const auto timeDiff = imageWithCoverage.imageMeta.capturedAt - edgeCoverage.actualizationDate;

            if (common::geometry::intersects(
                    imageWithCoverage.subpolyline, subpolyline))
            {
                intersectedSubpolylines.push_back(std::move(subpolyline));

                if (timeDiff > FRESHNESS_THRESHOLD) {
                    return true;
                }
            }
        }
    }

    intersectedSubpolylines = common::geometry::merge(std::move(intersectedSubpolylines));

    for (const auto& subpolyline : intersectedSubpolylines) {
        if (common::geometry::contains(subpolyline, imageWithCoverage.subpolyline)) {
            return false;
        }
    }

    return true;
}

std::optional<chrono::TimePoint>
evalMedianImageAge(const std::vector<ImageWithCoverage>& imagesWithCoverage)
{
    if (imagesWithCoverage.empty()) {
        return std::nullopt;
    }
    std::vector<chrono::TimePoint> capturedAtValues;
    capturedAtValues.reserve(imagesWithCoverage.size());

    for (const auto& item : imagesWithCoverage) {
        capturedAtValues.push_back(item.imageMeta.capturedAt);
    }
    auto m = capturedAtValues.begin() + capturedAtValues.size() / 2;
    std::nth_element(capturedAtValues.begin(), m, capturedAtValues.end());
    return *m;
}

std::vector<NexarImageMeta>
selectImagesThatImproveCurrentCoverage(
    const std::vector<ImageWithCoverage>& imagesWithCoverage,
    const fb::GraphReader& mrcGraphReader)
{
    std::set<FrameId> addedFrameIds;
    std::vector<NexarImageMeta> result;
    result.reserve(imagesWithCoverage.size());

    auto addImageToResult = [&](const NexarImageMeta& imageMeta) {
        if (addedFrameIds.insert(imageMeta.frameId).second) {
            result.push_back(imageMeta);
        }
    };

    for (const auto& imageWithCoverage : imagesWithCoverage) {
        auto edge = mrcGraphReader.edgeById(imageWithCoverage.edgeId.value());
        if (!edge.has_value()) {
            /// There was no coverage for this edge
            addImageToResult(imageWithCoverage.imageMeta);
        } else if (improvesEdgeCoverage(imageWithCoverage, edge.value())) {
            addImageToResult(imageWithCoverage.imageMeta);
        }
    }
    return result;
}

db::Feature convertToFeature(const NexarImageMeta& meta, const std::string& image)
{
    return sql_chemistry::GatewayAccess<db::Feature>::construct()
        .setTimestamp(meta.capturedAt)
        .setSourceId(meta.frameId)
        .setDataset(db::Dataset::NexarDashcams)
        .setGeodeticPos(meta.position)
        .setHeading(meta.heading)
        .setUploadedAt(chrono::TimePoint::clock::now())
        .setSize(common::sizeOf(common::decodeImage(image)));
}

db::Feature importImage(
    pgpool3::Pool& pool,
    const INexarClient& nexarClient,
    mds::Mds& mdsClient,
    const NexarImageMeta& meta)
{
    auto txn = pool.masterWriteableTransaction();
    auto image = nexarClient.loadImage(meta.imageUrl);
    auto feature = convertToFeature(meta, image);
    db::FeatureGateway gtw{*txn};
    gtw.insert(feature);

    auto mdsPath = common::makeMdsPath(
        common::MdsObjectSource::Imported,
        common::MdsObjectType::Image,
        feature.id());

    auto mdsResponse = mdsClient.post(mdsPath, image);
    feature.setMdsKey(mdsResponse.key());
    gtw.update(feature, db::UpdateFeatureTxn::No);

    db::NexarFrameToFeature frameToFeature(meta.frameId, feature.id());
    db::NexarFrameToFeatureGateway(*txn).insert(frameToFeature);

    txn->commit();
    return feature;
}

db::NexarImportTileUpdateInfo importImagesForTile(
    pgpool3::Pool& pool,
    mds::Mds& mdsClient,
    const fb::GraphReader& mrcGraphReader,
    const road_graph::Graph& graph,
    const succinct_rtree::Rtree& rtree,
    const INexarClient& nexarClient,
    const TileImportParams& tile)
{
    INFO() << "Selecting best covering images";
    const auto imagesWithCoverage = selectBestCoveringFrames(graph, rtree, tile, nexarClient);
    const auto imagesMedianAge = evalMedianImageAge(imagesWithCoverage);
    auto imagesToImport = selectImagesThatImproveCurrentCoverage(imagesWithCoverage, mrcGraphReader);

    INFO() << "Found " << imagesToImport.size() << " images to import";

    const auto alreadyImportedFrameIds =
        filterAlreadyImportedFrameIds(*pool.slaveTransaction(), collectFrameIds(imagesToImport));

    INFO() << alreadyImportedFrameIds.size()
           << " images were already imported before";

    imagesToImport.erase(std::remove_if(
        imagesToImport.begin(), imagesToImport.end(), [&](const auto& image) {
            return alreadyImportedFrameIds.count(image.frameId);
        }), imagesToImport.end());

    INFO() << "Left " << imagesToImport.size() << " images to import";

    db::Features features;
    features.reserve(imagesToImport.size());
    common::parallelForEach<NEXAR_CLIENT_CONCURRENCY>(
        imagesToImport.begin(),
        imagesToImport.end(),
        [&](std::mutex& mutex, const NexarImageMeta& meta) {
            MAPS_LOG_THREAD_PREFIX_APPEND(makeCurrentThreadLogPrefix());
            try {
                auto feature = importImage(pool, nexarClient, mdsClient, meta);
                std::lock_guard<std::mutex> lock(mutex);
                features.push_back(std::move(feature));
            } catch (const Exception& ex) {
                ERROR() << "Failed to import image " << meta << ": " << ex;
                throw;
            } catch (const std::exception& ex) {
                ERROR() << "Failed to import image " << meta << ": "
                        << ex.what();
                throw;
            }
        });

    INFO() << "Imported " << features.size() << " features";
    REQUIRE(features.size() == imagesToImport.size(),
        "Some of the images were not imported for tile " << tile);
    const auto now = chrono::TimePoint::clock::now();
    std::optional<int32_t> imagesMedianAgeDays;
    if (imagesMedianAge.has_value()) {
        imagesMedianAgeDays = std::chrono::duration_cast<std::chrono::days>(
            now - imagesMedianAge.value()).count();
    }
    return db::NexarImportTileUpdateInfo{
        tile.geoId, tile.fc, tile.x, tile.y, tile.z, now, imagesMedianAgeDays};
}

} // namespace maps::mrc::import_nexar
