#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/chrono/include/time_point.h>

#include <mapreduce/yt/interface/client.h>
#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>


#include <maps/wikimap/mapspro/services/mrc/libs/config/include/config.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/house_number_gateway.h>
#include <maps/libs/pgpool/include/pgpool3.h>

#include <opencv2/opencv.hpp>

#include <random>
#include <string>
#include <list>
#include <fstream>

using namespace NYT;

namespace {
struct FeedbackItem {
    maps::mrc::db::TId id;
    maps::geolib3::Point2 position; // geo
    TString number;
    bool accepted;
    bool commitsExists;
    bool fromPanoram;
};

struct FeedbackPartsStatistic {
    FeedbackPartsStatistic()
        : bbox(maps::geolib3::Point2(-1., -1.), maps::geolib3::Point2(-1., -1.))
        , parsedCnt(0)
        , acceptedCnt(0)
        , withCommitCnt(0)
    {}
    maps::geolib3::BoundingBox bbox; // geo
    uint64_t parsedCnt;
    uint64_t acceptedCnt;
    uint64_t withCommitCnt;
};

struct Neighbors {
    bool operator()(const FeedbackItem& a, const FeedbackItem& b) {
        static const double SEARCH_RADIUS_METERS = 10000;
        const double mercatorRadius = maps::geolib3::toMercatorUnits(SEARCH_RADIUS_METERS, geoPoint2Mercator(a.position));
        return maps::geolib3::squaredDistance( geoPoint2Mercator(a.position),  geoPoint2Mercator(b.position)) < mercatorRadius * mercatorRadius;
    }
};

void saveIds(const std::string& path, const maps::mrc::db::TIds& ids) {
    std::ofstream ofs(path);
    for (size_t i = 0; i < ids.size(); i++) {
        ofs << ids[i] << "\n";
    }
}

maps::mrc::db::TIds feedbackIdsToHouseNumberIds(const maps::mrc::db::TIds& feedbackIds,  maps::pgpool3::Pool& pool)
{
    auto txn = pool.slaveTransaction();
    maps::mrc::db::HouseNumbers houseNumbers =
        maps::mrc::db::HouseNumberGateway(*txn).load(
            maps::mrc::db::table::HouseNumber::feedbackTaskId.in(std::move(feedbackIds)));

    maps::mrc::db::TIds houseNumberIds(houseNumbers.size());
    for (size_t i = 0; i < houseNumbers.size(); i++) {
        houseNumberIds[i] = houseNumbers[i].id();
    }
    return houseNumberIds;
}

class PointsSearcher {
public:
    typedef maps::geolib3::StaticGeometrySearcher<maps::geolib3::Point2, TString> InternalSearcher;
public:
    void insert(maps::geolib3::Point2 &&pt, const TString &name) {
        pts_.emplace_back(pt);
        searcher_.insert(&pts_.back(), name);
    }
    void build() {
        searcher_.build();
    }
    bool find(const maps::geolib3::Point2& pt, const TString& number) {
        constexpr double EPSILON_METERS = 0.1;
        const double epsilon = 2. * maps::geolib3::toMercatorUnits(EPSILON_METERS, pt);
        const InternalSearcher::SearchResult searchResult = find({pt, epsilon, epsilon});
        for (auto itr = searchResult.first; itr != searchResult.second; itr++) {
            if (number == itr->value()) {
                return true;
            }
        }
        return false;
    }
private:
    InternalSearcher searcher_;
    std::list<maps::geolib3::Point2> pts_;

    const InternalSearcher::SearchResult find(const maps::geolib3::BoundingBox& searchBox) const {
        return searcher_.find(searchBox);
    }
};

}

int main(int argc, const char** argv) try {
    static const TString YT_PROXY = "hahn";
    static const TString YT_TABLE = "//home/maps/core/nmaps/analytics/feedback/db/feedback_latest";

    static const TString COL_ID         = "id";
    static const TString COL_TYPE       = "type";
    static const TString COL_SOURCE     = "source";
    static const TString COL_RESOLUTION = "resolution";
//    static const TString COL_WORKFLOW   = "workflow";
    static const TString COL_COMMIT_IDS = "commit_ids";
    static const TString COL_POSITION   = "position";
    static const TString COL_CREATED_AT = "created_at";
    static const TString COL_RESOLVED_AT  = "resolved_at";

    static const TString FEEDBACK_TYPE     = "address";
    static const TString FEEDBACK_SOURCE   = "mrc";
    static const TString FEEDBACK_ACCEPTED = "accepted";

    Initialize(argc, argv);

    maps::cmdline::Parser parser("Calculate house number feedback statistic");

    maps::cmdline::Option<std::string>
        createdBeforeParam =
            parser.string("created-before")
                  .defaultValue("2030-08-01 00:00:00.000000+03")
                  .help("Check hypotheses created before this date (default: 2030-08-01 00:00:00.000000+03)");

    maps::cmdline::Option<std::string>
        createdAfterParam =
            parser.string("created-after")
                  .defaultValue("2000-08-01 00:00:00.000000+03")
                  .help("Check hypotheses created after this date (default: 2000-08-01 00:00:00.000000+03)");

    maps::cmdline::Option<std::string>
        closedBeforeParam =
            parser.string("closed-before")
                  .defaultValue("2030-08-01 00:00:00.000000+03")
                  .help("Check hypotheses closed before this date (default: 2030-08-01 00:00:00.000000+03)");

    maps::cmdline::Option<std::string>
        closedAfterParam =
            parser.string("closed-after")
                  .defaultValue("2000-08-01 00:00:00.000000+03")
                  .help("Check hypotheses closed after this date (default: 2000-08-01 00:00:00.000000+03)");

    maps::cmdline::Option<std::string>
        outputAcceptedIDPath =
            parser.string("accepted")
                  .help("Path to output file for save accepted feedback/house_number ids");

    maps::cmdline::Option<std::string>
        outputWithCommitIDPath =
            parser.string("with-commit")
                  .help("Path to output file for save with commit feedback/house_number ids");

    maps::cmdline::Option<std::string>
        outputRejectedIDPath =
            parser.string("rejected")
                  .help("Path to output file for save rejected feedback/house_number ids");

    maps::cmdline::Option<std::string>
        mrcConfigPath =
            parser.string("mrc-config")
                .help("Path to mrc config");

    maps::cmdline::Option<std::string>
        secretVersion =
            parser.string("secret-version")
                .defaultValue("")
                .help("version for secrets from yav.yandex-team.ru");

    maps::cmdline::Option<bool>
        houseNumberParam =
            parser.flag("house-number")
                .help("Save house number ids (default: save feedback ids)");

    maps::cmdline::Option<std::string>
        outputImagePath =
            parser.string("output")
                  .help("Path to output image file");

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

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

    maps::chrono::TimePoint createdBefore = maps::chrono::parseSqlDateTime(createdBeforeParam);
    maps::chrono::TimePoint createdAfter  = maps::chrono::parseSqlDateTime(createdAfterParam);
    maps::chrono::TimePoint closedBefore  = maps::chrono::parseSqlDateTime(closedBeforeParam);
    maps::chrono::TimePoint closedAfter   = maps::chrono::parseSqlDateTime(closedAfterParam);

    uint64_t unparsedItemCount = 0;
    PointsSearcher pointsSearcher;
    std::vector<FeedbackItem> feedbackItems;
    TTableReaderPtr<TNode> reader = client->CreateTableReader<TNode>(YT_TABLE);
    for (int processedItems = 0; reader->IsValid(); reader->Next(), processedItems++) {
        if ((processedItems + 1) % 1000000 == 0) {
            INFO() << "Processed " << (processedItems + 1) << " items";
        }

        const TNode& inpRow = reader->GetRow();
        if (inpRow[COL_TYPE].AsString() != FEEDBACK_TYPE || inpRow[COL_SOURCE].AsString() != FEEDBACK_SOURCE)
            continue;

        maps::chrono::TimePoint createdAt = maps::chrono::parseSqlDateTime(inpRow[COL_CREATED_AT].AsString());
        if (createdAt >= createdBefore || createdAt <= createdAfter)
            continue;
        if (inpRow[COL_RESOLUTION].IsNull()) {
            unparsedItemCount++;
            continue;
        }

        if (inpRow[COL_RESOLVED_AT].IsNull())
            continue;
        maps::chrono::TimePoint closedAt = maps::chrono::parseSqlDateTime(inpRow[COL_RESOLVED_AT].AsString());
        if (closedAt >= closedBefore || closedAt <= closedAfter)
            continue;

        //maps::geolib3::convertMercatorToGeodetic(mercatorPos());

        FeedbackItem item;
        item.id = inpRow[COL_ID].AsUint64();
        item.accepted = (inpRow[COL_RESOLUTION].AsString() == FEEDBACK_ACCEPTED);
        item.commitsExists = (0 != inpRow[COL_COMMIT_IDS].AsList().size());
        item.position = maps::geolib3::Point2(inpRow[COL_POSITION].AsList()[0].AsDouble(), inpRow[COL_POSITION].AsList()[1].AsDouble());
        item.number = inpRow["attrs"].AsMap().at("addressHouseNumber").AsString();
        const TVector<TNode>& imageFeatures = inpRow["attrs"].AsMap().at("sourceContext").AsMap().at("content").AsMap().at("imageFeatures").AsList();
        item.fromPanoram = false;
        for (const TNode& feature : imageFeatures) {
            if (TString::npos != feature.AsMap().at("imageFull").AsMap().at("url").AsString().find("pano")) {
                item.fromPanoram = true;
                break;
            }
        }
        feedbackItems.emplace_back(item);
        if (item.commitsExists)
            pointsSearcher.insert(maps::geolib3::convertGeodeticToMercator(item.position), item.number);
    };
    pointsSearcher.build();

    uint64_t acceptedItemCount = 0;
    uint64_t commitedItemCount = 0;
    uint64_t doubledItemCount = 0;
    uint64_t fromPanoramAcceptedItemCount = 0;
    uint64_t fromPanoramCommitedItemCount = 0;
    uint64_t fromPanoramItemCount = 0;
    for (const FeedbackItem& item : feedbackItems) {
        if (item.fromPanoram)
            fromPanoramItemCount++;
        if (item.commitsExists) {
            commitedItemCount++;
            if (item.fromPanoram)
                fromPanoramCommitedItemCount++;
        } else {
            if (pointsSearcher.find(maps::geolib3::convertGeodeticToMercator(item.position), item.number)) {
                doubledItemCount++;
                if (item.fromPanoram)
                    fromPanoramItemCount--;
                continue;
            }
        }
        if (item.accepted) {
            acceptedItemCount++;
            if (item.fromPanoram)
                fromPanoramAcceptedItemCount++;
        }
    }

    std::cout << "Unparsed items:      " << unparsedItemCount << std::endl;
    std::cout << "Parsed items:        " << feedbackItems.size() - doubledItemCount
              << " (+ doubled: " << doubledItemCount << " = " << feedbackItems.size() << ")" << std::endl;
    std::cout << "  Accepted items:    " << acceptedItemCount << std::endl;
    std::cout << "  With commit items: " << commitedItemCount << std::endl;

    std::cout << "From panoram" << std::endl;
    std::cout << "Parsed items:        " << fromPanoramItemCount << std::endl;
    std::cout << "  Accepted items:    " << fromPanoramAcceptedItemCount << std::endl;
    std::cout << "  With commit items: " << fromPanoramCommitedItemCount++ << std::endl;

    if (!outputImagePath.empty())
    {
        std::vector<int> labels;
        int lblsCnt = cv::partition(feedbackItems, labels, Neighbors());
        std::vector<FeedbackPartsStatistic> parts(lblsCnt );
        for (size_t i = 0; i < feedbackItems.size(); i++) {
            //std::cout << feedbackItems[i].position.x() << ", " << feedbackItems[i].position.y() << std::endl;
            if (parts[labels[i]].bbox.minX() < 0.) {
                parts[labels[i]].bbox = maps::geolib3::BoundingBox(feedbackItems[i].position, feedbackItems[i].position);
            } else {
                parts[labels[i]].bbox = maps::geolib3::expand(parts[labels[i]].bbox, feedbackItems[i].position);
            }
            parts[labels[i]].parsedCnt ++;
            if (feedbackItems[i].accepted)
                parts[labels[i]].acceptedCnt ++;
            if (feedbackItems[i].commitsExists)
                parts[labels[i]].withCommitCnt ++;
        }

        std::sort(parts.begin(), parts.end(),
            [](const FeedbackPartsStatistic& a, const FeedbackPartsStatistic& b)
            { return a.parsedCnt > b.parsedCnt; } );

        for (int i = 0; i < lblsCnt; i++) {
            std::cout << i << "."
                      << " [" << parts[i].bbox.minX() << ", "
                      << parts[i].bbox.minY() << "]"
                      << " - [" << parts[i].bbox.maxX() << ", "
                      << parts[i].bbox.maxY() << "]" << std::endl;
            std::cout << "Parsed items:        " << parts[i].parsedCnt << std::endl;
            std::cout << "  Accepted items:    " << parts[i].acceptedCnt << std::endl;
            std::cout << "  With commit items: " << parts[i].withCommitCnt << std::endl;
            std::cout << std::endl;
        }

        const double minx = 20.;
        const double maxx = 100.;
        const double miny = 30.;
        const double maxy = 70.;
        cv::Mat img((int)((maxy - miny) * 20.), (int)((maxx - minx) * 20.), CV_8UC3, cv::Scalar(0, 0, 0));
        for (int i = 0; i < lblsCnt; i++) {
            if (parts[i].parsedCnt < 100)
                continue;

            const double quality = (double)parts[i].withCommitCnt / (double)parts[i].parsedCnt;
            cv::Scalar color(0, 255, 0);
            if (quality < 0.1)
                color = cv::Scalar(0, 0, 255);
            else if (quality < 0.5)
                color = cv::Scalar(0, 128, 255);
            else if (quality < 0.9)
                color = cv::Scalar(0, 255, 255);

            cv::rectangle(  img,
                            cv::Point(20. * (parts[i].bbox.minX() - minx), 20. * (parts[i].bbox.minY() - miny)),
                            cv::Point(20. * (parts[i].bbox.maxX() - minx), 20. * (parts[i].bbox.maxY() - miny)),
                            color, -1);
        }
        cv::imwrite(outputImagePath.c_str(), img);
    }


    const maps::mrc::common::Config mrcConfig =
        maps::mrc::common::templateConfigFromCmdPath(secretVersion, mrcConfigPath);
    auto poolHolder = mrcConfig.makePoolHolder();

    if (!outputAcceptedIDPath.empty()) {
        maps::mrc::db::TIds ids;
        for (size_t i = 0; i < feedbackItems.size(); i++) {
            if (feedbackItems[i].accepted)
                ids.push_back(feedbackItems[i].id);
        }
        if (houseNumberParam)
            ids = feedbackIdsToHouseNumberIds(ids, poolHolder.pool());
        saveIds(outputAcceptedIDPath, ids);
    }
    if (!outputWithCommitIDPath.empty()) {
        maps::mrc::db::TIds ids;
        for (size_t i = 0; i < feedbackItems.size(); i++) {
            if (feedbackItems[i].commitsExists)
                ids.push_back(feedbackItems[i].id);
        }
        if (houseNumberParam)
            ids = feedbackIdsToHouseNumberIds(ids, poolHolder.pool());
        saveIds(outputWithCommitIDPath, ids);
    }
    if (!outputRejectedIDPath.empty()) {
        maps::mrc::db::TIds ids;
        for (size_t i = 0; i < feedbackItems.size(); i++) {
            if (!feedbackItems[i].accepted)
                ids.push_back(feedbackItems[i].id);
        }
        if (houseNumberParam)
            ids = feedbackIdsToHouseNumberIds(ids, poolHolder.pool());
        saveIds(outputRejectedIDPath, ids);
    }
    return EXIT_SUCCESS;
}
catch (const maps::Exception& e) {
    FATAL() << "Tools failed: " << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    FATAL() << "Tools failed: " << e.what();
    return EXIT_FAILURE;
}
