#include "st_notifier.h"

#include <util/string/cast.h>
#include <maps/libs/common/include/profiletimer.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/string_utils.h>
#include <maps/wikimap/mapspro/services/tasks_realtime/src/user_edits_metrics/lib/metrics_calculator_impl.h>

#include <chrono>

namespace maps::wiki::user_edits_metrics {

using namespace std::string_literals;

namespace {

std::string daysNumber(const maps::chrono::Days& days)
{
    return std::to_string(days.count());
}

std::string daysNumber(const std::chrono::seconds& value)
{
    const auto days = std::chrono::duration_cast<maps::chrono::Days>(value);
    return std::to_string(days.count());
}

std::string eventToString(const Event& event)
{
    return std::string(toString(event));
}

std::string bold(const std::string& str)
{
    return "**"s + str + "**"s;
}

std::optional<std::chrono::seconds> findBrokenSlaValue(
    const maps::chrono::TimePoint& dayBegin,
    const MetricVec& metrics)
{
    for (const auto& metric: metrics) {
        if (dayBegin == metric.time) {
            return metric.value;
        }
    }
    return {};
}

maps::chrono::Days brokenSlaIncreasedTo(
    const maps::chrono::TimePoint& dayBegin,
    const MetricVec& metrics)
{
    for (auto metricsIt = metrics.begin(); metricsIt != metrics.end(); ++metricsIt) {
        if (dayBegin == metricsIt->time) {
            if (metricsIt != metrics.begin() && metricsIt->value > std::prev(metricsIt)->value) {
                return std::chrono::duration_cast<maps::chrono::Days>(metricsIt->value);
            } else {
                return {};
            }
        }
    }
    return {};
}

void printDeployedEventsToLog(const RegionToEvents& regionToDeployedEvents, const std::string& region)
{
    INFO() << "Deployed events in region " << region << ':';
    for (const auto& event: regionToDeployedEvents.at(region)) {
        INFO() << eventToString(event);
    }
}

void printBrokenSlaToLog(const MetricVec& brokenSlaTotal, const std::map<Event, MetricVec>& brokenSla)
{
    for (const auto& metric: brokenSlaTotal) {
        INFO() << maps::chrono::formatIsoDate(metric.time) << " Broken SLA Total: "
               << daysNumber(metric.value) << " days";
    }
    for (const auto& [event, metricsVec]: brokenSla) {
        for (const auto& metric: metricsVec) {
            INFO() << maps::chrono::formatIsoDate(metric.time) << ' ' << eventToString(event) << ": "
                   << daysNumber(metric.value) << " days";
        }
    }
}

void eraseSince(const chrono::TimePoint& lastTrustedDayBegin, const Event& event, MetricVec& metric)
{
    auto metricsIt = metric.rbegin();
    for (; metricsIt != metric.rend(); ++metricsIt) {
        const auto& dayBegin = metricsIt->time;
        if (dayBegin <= lastTrustedDayBegin) {
            break;
        }
    }
    INFO() << "Cut off " << std::distance(metricsIt.base(), metric.end())
           << " from " << eventToString(event) << " to align all metrics by days";
    metric.erase(metricsIt.base(), metric.end());
}

} // namespace

void cutOffUntrustedBrokenSlaTotal(
    MetricVec& brokenSlaTotal,
    std::map<Event, MetricVec>& brokenSla,
    const EventSet& deployedEvents
)
{
    for (const auto& event: deployedEvents) {
        if (brokenSla.count(event) == 0) {
            brokenSlaTotal.clear();
            return;
        }
    }

    auto lastTrustedDayBegin = brokenSlaTotal.back().time;
    for (const auto& event: deployedEvents) {
        const auto& dayBegin = brokenSla[event].back().time;
        lastTrustedDayBegin = std::min(lastTrustedDayBegin, dayBegin);
    }

    eraseSince(lastTrustedDayBegin, Event::DeployedTotal, brokenSlaTotal);
    for (const auto& event: deployedEvents) {
        eraseSince(lastTrustedDayBegin, event, brokenSla[event]);
    }
}

NotifySettings::NotifySettings()
    : quantile(0.85)
    , inTrunk(true)
    , region("cis1")
    , userType(USER_TYPE_COMMON)
    , windowSize(30)
    , sla(5_days)
    , slaWarningLevel(3_days)
    , deployedEvents(DEPLOYED_TO_PRODUCTION_EVENTS)
    , queueKey("MAPSLSR")
    , summary("Предупреждения о нарушениях SLA деплоя данных")
    , referenceToGraphics("https://datalens.yandex-team.ru/bq3fxpmya6e67-kpi?tab=yd0")
    , followers({"alexbobkov", "tail"})
{}

void checkMetricsAndNotifySt(
    const MetricVec& allMetrics,
    const RegionToEvents& regionToDeployedEvents,
    const maps::chrono::TimePoint& calculationDate,
    bool upload)
{
    if (!upload) {
        return;
    }
    INFO() << "Start ST notifier logic.";
    ProfileTimer timer;

    const NotifySettings settings;
    printDeployedEventsToLog(regionToDeployedEvents, settings.region);
    if (
        regionToDeployedEvents.count(settings.region) > 0
        && regionToDeployedEvents.at(settings.region) == settings.deployedEvents
    ) {
        MetricPrepare prepare(settings);
        auto brokenSlaTotal = prepare.calcBrokenSlaTotalMetrics(
            allMetrics,
            calculationDate
        );
        auto brokenSla = prepare.calcBrokenSlaMetrics(
            allMetrics,
            calculationDate
        );
        INFO() << "Broken SLA metrics preparation finished";
        printBrokenSlaToLog(brokenSlaTotal, brokenSla);
        cutOffUntrustedBrokenSlaTotal(brokenSlaTotal, brokenSla, settings.deployedEvents);
        if (!brokenSlaTotal.empty() && !brokenSla.empty()) {
            StAgent stAgent(settings.queueKey);
            StNotifier notifier(settings, brokenSlaTotal, brokenSla);
            notifier.doNotifyLogic(stAgent);
        }
    }
    INFO() << "End ST notifier logic. Time duration = " << timer << " seconds.";
}

MetricPrepare::MetricPrepare(const NotifySettings& settings)
    : settings_(settings)
{}

MetricVec MetricPrepare::calcBrokenSlaTotalMetrics(
    const MetricVec& allMetrics,
    const maps::chrono::TimePoint& calculationDate) const
{
    auto deployedMetrics = getDeployedTotalMetricsWithEstimations(
        allMetrics,
        calculationDate
    );

    auto result = getDaysOutOfSlaMetrics(
        deployedMetrics,
        calculationDate,
        Event::DeployedTotal);
    std::for_each(result.begin(), result.end(), [](Metric& metric) {
        const auto [dayBegin, _] = chrono::getComprisingTimeSpan<chrono::Days>(metric.time);
        metric.time = dayBegin;
    });

    return result;
}

std::map<Event, MetricVec> MetricPrepare::calcBrokenSlaMetrics(
    const MetricVec& allMetrics,
    const maps::chrono::TimePoint& calculationDate) const
{
    std::map<Event, MetricVec> result;
    for (const auto& event: settings_.deployedEvents) {
        auto deployedMetrics = impl::getConsolidatedByDaysMetricsWithEstimations(
            allMetrics,
            settings_.quantile,
            settings_.inTrunk,
            settings_.region,
            settings_.userType,
            event,
            calculationDate
        );

        result[event] = getDaysOutOfSlaMetrics(deployedMetrics, calculationDate, event);
        std::for_each(result[event].begin(), result[event].end(), [](Metric& metric) {
            const auto [dayBegin, _] = chrono::getComprisingTimeSpan<chrono::Days>(metric.time);
            metric.time = dayBegin;
        });
    }
    return result;
}

MetricVec MetricPrepare::getDeployedTotalMetricsWithEstimations(
    const MetricVec& allMetrics,
    const maps::chrono::TimePoint& calculationDate) const
{
    const auto deployedMetrics = impl::getMetrics(
        allMetrics,
        settings_.quantile,
        settings_.inTrunk,
        settings_.region,
        settings_.userType,
        settings_.deployedEvents
    );
    auto consolidatedMetrics = impl::sortAndConsolidateDeployedMetricsByDays(
        deployedMetrics,
        Event::DeployedTotal,
        settings_.deployedEvents
    );
    consolidatedMetrics = impl::addEstimatedMissingMetrics(std::move(consolidatedMetrics), calculationDate);
    return consolidatedMetrics;
}

MetricVec MetricPrepare::getDaysOutOfSlaMetrics(
    MetricVec& sortedMetrics,
    const maps::chrono::TimePoint& calculationDate,
    const Event& event) const
{
    sortedMetrics = impl::dropEstimationsInSlaWindow(std::move(sortedMetrics), calculationDate, settings_.sla);
    return impl::daysOutOfSlaInSlidingWindow(
        sortedMetrics,
        EVENT_TO_BROKEN_SLA_EVENT.at(event),
        settings_.windowSize,
        settings_.quantile,
        settings_.inTrunk,
        settings_.region,
        settings_.userType);
}

StNotifier::StNotifier(
    const NotifySettings& settings,
    MetricVec& brokenSlaTotal,
    std::map<Event, MetricVec>& brokenSla)
    : brokenSlaTotal_(brokenSlaTotal)
    , brokenSla_(brokenSla)
    , settings_(settings)
{
    eraseMetricsBeforeCrossingWarningLevel();
}

void StNotifier::eraseMetricsBeforeCrossingWarningLevel()
{
    if (brokenSlaTotal_.back().value < settings_.slaWarningLevel) {
        return;
    }
    for (auto itr = brokenSlaTotal_.crbegin() + 1; itr != brokenSlaTotal_.crend(); ++itr) {
        if (itr->value < settings_.slaWarningLevel) {
            brokenSlaTotal_.erase(brokenSlaTotal_.cbegin(), itr.base());
            dayOfCrossingWarningLevelFound_ = true;
            break;
        }
    }
    const auto dayStartWarning = brokenSlaTotal_.front().time;
    for (auto& [_, brokenSlaForModule]: brokenSla_) {
        for (auto itr = brokenSlaForModule.crbegin(); itr != brokenSlaForModule.crend(); ++itr) {
            if (itr->time < dayStartWarning) {
                brokenSlaForModule.erase(brokenSlaForModule.begin(), itr.base());
                break;
            }
        }
    }
}

std::string StNotifier::generateComment(const maps::chrono::TimePoint& fromTime) const
{
    std::map<maps::chrono::TimePoint, std::vector<std::string>> commentsByDays;

    auto itrBrokenSlaTotal = brokenSlaTotal_.begin();
    if (dayOfCrossingWarningLevelFound_ && fromTime <= itrBrokenSlaTotal->time) {
        // In the first exceeded of total warning SLA level we notify about all broken SLA in that day
        const auto& dayBegin = itrBrokenSlaTotal->time;
        commentsByDays[dayBegin].push_back(timeToDate(itrBrokenSlaTotal->time));
        commentsByDays[dayBegin].push_back(
                "Количество дней, в которых нарушался SLA, достигло значения " + daysNumber(settings_.slaWarningLevel)
        );
        for (const auto& [event, metrics]: brokenSla_) {
            const auto brokenSla = findBrokenSlaValue(dayBegin, metrics);
            if (brokenSla && brokenSla.value() > 0_days) {
                commentsByDays[dayBegin].push_back(
                        "Нарушений SLA " + bold(eventToString(event)) + " = " + daysNumber(brokenSla.value())
                );
            }
        }
    }
    ++itrBrokenSlaTotal;

    for (; itrBrokenSlaTotal != brokenSlaTotal_.end(); ++itrBrokenSlaTotal) {
        // After exceeded of total warning SLA level we notify only about increased broken SLA
        // that affected increasing of total broken SLA
        if (itrBrokenSlaTotal->value > std::prev(itrBrokenSlaTotal)->value
            && fromTime <= itrBrokenSlaTotal->time) {
            const auto& dayBegin = itrBrokenSlaTotal->time;
            commentsByDays[dayBegin].push_back(timeToDate(itrBrokenSlaTotal->time));
            commentsByDays[dayBegin].push_back(
                    "Количество дней, в которых нарушался SLA, увеличилось до " + daysNumber(itrBrokenSlaTotal->value)
            );
            for (const auto& [event, metrics]: brokenSla_) {
                const auto days = brokenSlaIncreasedTo(dayBegin, metrics);
                if (days.count() > 0) {
                    commentsByDays[dayBegin].push_back(
                            "Количество нарушений SLA " + bold(eventToString(event))
                            + " достигло значения " + daysNumber(days)
                    );
                }
            }
        }
    }

    std::vector<std::string> resultByDays;
    for (const auto& [_, strings]: commentsByDays) {
        resultByDays.emplace_back(common::join(strings, '\n'));
    }
    return common::join(resultByDays, "\n\n");
}

std::string StNotifier::getDescription() const
{
    const auto description = "Количество дней, в которых нарушался SLA в скользящем окне."s
                             + "\nPercentile = "s + FloatToString(settings_.quantile)
                             + "\nIn Trunk = "s + std::to_string(settings_.inTrunk)
                             + "\nRegion = "s + settings_.region
                             + "\nUser type = "s + settings_.userType
                             + "\nSliding window = "s + std::to_string(settings_.windowSize)
                             + "\nBroken SLA budget = "s + daysNumber(settings_.sla)
                             + "\nSLA warning level = "s + daysNumber(settings_.slaWarningLevel)
                             + "\n\nБолее подробная информация здесь: " + settings_.referenceToGraphics;
    return description;
}

std::string StNotifier::timeToDate(const maps::chrono::TimePoint& timePoint)
{
    return maps::chrono::formatIsoDate(timePoint);
}

} // namespace maps::wiki::user_edits_metrics
