#include "stat.h"
#include "common.h"
#include "config.h"
#include "message.h"

#include <string>
#include <maps/libs/chrono/include/time_point.h>
#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/value.h>
#include <yandex/maps/wiki/common/secrets.h>
#include <maps/libs/http/include/http.h>
#include <maps/libs/log8/include/log8.h>
#include <util/system/env.h>
#include <util/string/strip.h>

namespace maps::wiki::merge_poi {

namespace {

const std::set<std::string> REJECT_STAT_MESSAGES = {
    message::MOVE_ADDR_COLLISION,
    message::MOVE_BLD_CHANGE,
    message::MOVE_DUPLICATE,
    message::MOVE_POI_COLLISION,
    message::MOVE_PROTECTED_BY_ATTRIBUTE
};

namespace col {
const auto FIELDDATE = "fielddate"s;
const auto EXPORTED = "exported"s;
const auto SPRAV = "sprav"s;
const auto OUTDATED = "outdated"s;
const auto PATCHES = "patches"s;
const auto FOR_PROTECTED = "for_protected"s;
const auto REJECTED = "rejected"s;
const auto SKIPPED = "skipped"s;
const auto NO_SETTINGS = "no_settings"s;
const auto DELETED = "deleted"s;
const auto MOVED = "moved"s;
const auto RENAMED = "renamed"s;
const auto FEEDBACK = "feedback"s;
const auto TO_CHANGE_RUBRIC = "to_change_rubric"s;
const auto TO_DELETE = "to_delete"s;
const auto TO_MOVE = "to_move"s;
const auto TO_RENAME = "to_rename"s;

const auto OVER_500_METERS_AWAY = "over_500_meters_away"s;
const auto OVER_100_METERS_AWAY = "over_100_meters_away"s;
const auto OVER_50_METERS_AWAY = "over_50_meters_away"s;
const auto OVER_20_METERS_AWAY = "over_20_meters_away"s;
const auto OVER_10_METERS_AWAY = "over_10_meters_away"s;
const auto OVER_5_METERS_AWAY = "over_5_meters_away"s;
const auto OVER_1_METERS_AWAY = "over_1_meters_away"s;

const auto MOVED_OVER_1_DAYS_OLD = "moved_over_1_days_old"s;
const auto MOVED_OVER_2_DAYS_OLD = "moved_over_2_days_old"s;
const auto MOVED_OVER_3_DAYS_OLD = "moved_over_3_days_old"s;
const auto MOVED_OVER_4_DAYS_OLD = "moved_over_4_days_old"s;
const auto MOVED_OVER_5_DAYS_OLD = "moved_over_5_days_old"s;
const auto MOVED_OVER_6_DAYS_OLD = "moved_over_6_days_old"s;
const auto MOVED_OVER_7_DAYS_OLD = "moved_over_7_days_old"s;

} // col

namespace stat {
const auto REPORT_NAME = "Maps.Wiki/Sprav/Merge.Poi.Worker"s;
const auto REJECTS_REPORT_NAME = "Maps.Wiki/Sprav/Merge.Poi.Worker.Rejected"s;
const auto NAME = "name"s;
const auto SCALE = "scale"s;
const auto SCALE_DAILY = "d"s;
const auto JSON_DATA = "json_data"s;
const auto VALUES = "values"s;
} // stat

const auto AUTHORIZATION_HEADER = "Authorization"s;
const auto OAUTH_PREFIX = "OAuth "s;

std::string statToken()
{
    auto token = Strip(GetEnv(STAT_TOKEN_ENV.c_str()));
    if (!token.empty()) {
        return token;
    }
    return common::secrets::tokenByKey(common::secrets::Key::RobotWikimapStatToken);
}

void
toJsonObject(
    json::ObjectBuilder& object,
    const CountersByDistance& byDistance,
    const std::string& columnPrefix = {})
{
    object[columnPrefix + col::OVER_500_METERS_AWAY] = byDistance.over500MetersAway;
    object[columnPrefix + col::OVER_100_METERS_AWAY] = byDistance.over100MetersAway;
    object[columnPrefix + col::OVER_50_METERS_AWAY] = byDistance.over50MetersAway;
    object[columnPrefix + col::OVER_20_METERS_AWAY] = byDistance.over20MetersAway;
    object[columnPrefix + col::OVER_10_METERS_AWAY] = byDistance.over10MetersAway;
    object[columnPrefix + col::OVER_5_METERS_AWAY] = byDistance.over5MetersAway;
    object[columnPrefix + col::OVER_1_METERS_AWAY] = byDistance.over1MetersAway;
}

} // namespace

std::string
RejectsStatData::toReportJson() const
{
    const auto fieldDate = maps::chrono::formatIsoDate(maps::chrono::TimePoint::clock::now());
    json::Builder reportBuilder;
    reportBuilder << [&](maps::json::ObjectBuilder report) {
        report[stat::VALUES] << [&](maps::json::ArrayBuilder values) {
            values << [&](maps::json::ObjectBuilder data) {
                data[col::FIELDDATE] = fieldDate;
                for (const auto& [message, counter] : countersByMessage) {
                    if (!message.starts_with(message::MOVE_PREFIX)) {
                        continue;
                    }
                    toJsonObject(data, counter, message + "_");
                }
            };
        };
    };
    return reportBuilder.str();
}

bool
RejectsStatData::post(const Config& cfg) const
{
    return postStat(cfg, stat::REJECTS_REPORT_NAME, toReportJson());
}

void RejectsStatData::build(const Config& cfg)
{
    auto socialTxn = cfg.socialPool().slaveTransaction();
    auto rows = socialTxn->exec(
        "SELECT messages, "
        " ST_DISTANCESPHERE("
        "   ST_SETSRID(ST_MAKEPOINT((e.data_json->>'lon')::float, (e.data_json->>'lat')::float),4326), "
        "   ST_SETSRID(ST_MAKEPOINT((a.data_json->>'lon')::float, (a.data_json->>'lat')::float),4326)) "
        " FROM " + EXPORT_POI_WORKER_FEED_DATA + " e "
        " INNER JOIN " + MERGE_POI_PATCH_QUEUE_ARCHIVE + " a "
        " USING (object_id) WHERE resolution = 'rejected';");
    for (const auto& row : rows) {
        if (row[0].is_null()) {
            continue;
        }
        const auto messagesJsonStr = row[0].as<std::string>();
        if (messagesJsonStr.empty()) {
            continue;
        }
        if (row[1].is_null()) {
            continue;
        }
        const auto messagesJson = json::Value::fromString(messagesJsonStr);
        ASSERT(messagesJson.isArray());
        const auto distance = row[1].as<double>();
        for (const auto& messageJson : messagesJson) {
            const auto message = messageJson.as<std::string>();
            if (!REJECT_STAT_MESSAGES.count(message)) {
                continue;
            }
            auto& counter = countersByMessage[message];
            if (distance > 500) {
                ++counter.over500MetersAway;
            }
            if (distance > 100) {
                ++counter.over100MetersAway;
            }
            if (distance > 50) {
                ++counter.over50MetersAway;
            }
            if (distance > 20) {
                ++counter.over20MetersAway;
            }
            if (distance > 10) {
                ++counter.over10MetersAway;
            }
            if (distance > 5) {
                ++counter.over5MetersAway;
            }
            if (distance > 1) {
                ++counter.over1MetersAway;
            }
        }
    }
}

std::string
StatData::toReportJson() const
{
    const auto fieldDate = maps::chrono::formatIsoDate(maps::chrono::TimePoint::clock::now());
    json::Builder reportBuilder;
    reportBuilder << [&](maps::json::ObjectBuilder report) {
        report[stat::VALUES] << [&](maps::json::ArrayBuilder values) {
            values << [&](maps::json::ObjectBuilder data) {
                data[col::FIELDDATE] = fieldDate;
                data[col::EXPORTED] = exported;
                data[col::SPRAV] = sprav;
                data[col::OUTDATED] = outdated;
                data[col::PATCHES] = patches;
                data[col::FOR_PROTECTED] = forProtected;
                data[col::REJECTED] = rejected;
                data[col::SKIPPED] = skipped;
                data[col::NO_SETTINGS] = noSettings;
                data[col::DELETED] = deleted;
                data[col::MOVED] = moved;
                data[col::RENAMED] = renamed;
                data[col::FEEDBACK] = feedback;

                data[col::TO_CHANGE_RUBRIC] = toChangeRubric;
                data[col::TO_DELETE] = toDelete;
                data[col::TO_MOVE] = toMove;
                data[col::TO_RENAME] = toRename;

                toJsonObject(data, toMoveByDistance);

                data[col::MOVED_OVER_1_DAYS_OLD] = movedAge1d;
                data[col::MOVED_OVER_2_DAYS_OLD] = movedAge2d;
                data[col::MOVED_OVER_3_DAYS_OLD] = movedAge3d;
                data[col::MOVED_OVER_4_DAYS_OLD] = movedAge4d;
                data[col::MOVED_OVER_5_DAYS_OLD] = movedAge5d;
                data[col::MOVED_OVER_6_DAYS_OLD] = movedAge6d;
                data[col::MOVED_OVER_7_DAYS_OLD] = movedAge7d;

            };
        };
    };
    return reportBuilder.str();
}

bool
StatData::post(const Config& cfg) const
{
    return postStat(cfg, stat::REPORT_NAME, toReportJson());
}

size_t
StatData::increaseFeedbackByType(FtTypeId ftTypeId)
{
    std::lock_guard<std::mutex> lock(feedbackTasksByTypeMutex);
    return ++feedbackTasksByType[ftTypeId];
}

void
StatData::log() const
{
    INFO() << "***Feedback tasks by ft_type BEGIN***";
    for (const auto& [ftTypeId, tasksCount] : feedbackTasksByType) {
        INFO() << "FT_TYPE: " << ftTypeId << " COUNT: " << tasksCount;
    }
    INFO() << "**Feedback tasks by ft_type END ***";
}

void
StatData::increaseMovedAge(size_t daysAge)
{
    if (daysAge > 1) {
        ++movedAge1d;
    }
    if (daysAge > 2) {
        ++movedAge2d;
    }
    if (daysAge > 3) {
        ++movedAge3d;
    }
    if (daysAge > 4) {
        ++movedAge4d;
    }
    if (daysAge > 5) {
        ++movedAge5d;
    }
    if (daysAge > 6) {
        ++movedAge6d;
    }
    if (daysAge > 7) {
        ++movedAge7d;
    }
}

bool
postStat(const Config& cfg, const std::string& reportName, const std::string& reportBody)
{
    INFO() << "Report name: " << reportName << " JSON:" << reportBody;
    const auto& url = cfg.statUrl();
    http::Client client;
    http::Request request(client, http::POST, url);
    request.addParam(stat::JSON_DATA, reportBody);
    request.addParam(stat::NAME, reportName);
    request.addParam(stat::SCALE, stat::SCALE_DAILY);
    request.addHeader(AUTHORIZATION_HEADER, OAUTH_PREFIX + statToken());
    request.addHeader("Connection", "close");
    auto response = request.perform();
    if (response.status() != HTTP_OK) {
        ERROR() << "Failed to post stat: " << reportBody
            << " to " << url << "\n" << response.readBody();
        return false;
    }
    INFO() << "Report successefuly submited to " << url;
    return true;
}


} // maps::wiki::merge_poi
