#include <balancer/serval/core/config.h>

#include <array>

static NSv::TAction Report(const YAML::Node& args, NSv::TAuxData& aux) {
    static constexpr double SCALE = 1e6;

    struct TReportStats {
        TString Uuid;

        // Independent stats
        NSv::TNumber<ui64>* Requests = nullptr;
        std::array<NSv::TNumber<ui64>*, 5> OutgoingCodeCategs;
        THashMap<int, NSv::TNumber<ui64>*> OutgoingCodes;

        // Stats from stream
        NSv::TNumber<ui64>* BackendAttempts = nullptr;
        NSv::TNumber<ui64>* BackendError = nullptr;
        NSv::TNumber<ui64>* BackendTimeout = nullptr;
        NSv::TNumber<ui64>* ConnRefused = nullptr;
        NSv::TNumber<ui64>* ConnTimeout = nullptr;
        NSv::TNumber<ui64>* ConnReset = nullptr;
        std::array<NSv::TNumber<ui64>*, 5> BackendStatusCodeCategs;

        NSv::THistogram* ConnectTimes = nullptr;
        NSv::THistogram* BackendTimes = nullptr;
        NSv::THistogram* ProcessingTimes = nullptr;

        TMaybe<TString> ConnectTimeHeader;
        TMaybe<TString> BackendTimeHeader;

        TReportStats(const TString& uuid, NSv::TAuxData& aux) {
            Uuid = uuid;
            for (size_t i = 1; i <= 5; ++i) {
                OutgoingCodeCategs[i - 1] = &aux.CustomSignal(
                    TStringBuilder{} << "report-" << Uuid << "-outgoing_" << i << "xx_summ"
                );
                BackendStatusCodeCategs[i - 1] = &aux.CustomSignal(
                    TStringBuilder{} << "report-" << Uuid << "-backend-sc_" << i << "xx_summ"
                );
            }
            Requests = &aux.CustomSignal(TStringBuilder{} << "report-" << Uuid << "-requests_summ");
            BackendAttempts = &aux.CustomSignal(TStringBuilder{} << "report-" << Uuid << "-backend_attempts_summ");
            BackendError = &aux.CustomSignal(TStringBuilder{} << "report-" << Uuid << "-backend_error_summ");
            BackendTimeout = &aux.CustomSignal(TStringBuilder{} << "report-" << Uuid << "-backend_timeout_summ");
            ConnRefused = &aux.CustomSignal(TStringBuilder{} << "report-" << Uuid << "-conn_refused_summ");
            ConnTimeout = &aux.CustomSignal(TStringBuilder{} << "report-" << Uuid << "-conn_timeout_summ");
            ConnReset = &aux.CustomSignal(TStringBuilder{} << "report-" << Uuid << "-conn_reset_summ");
        }

        void AddOutgoingCode(int code, NSv::TAuxData& aux) {
            OutgoingCodes[code] = &aux.CustomSignal(
                TStringBuilder{} << "report-" << Uuid << "-outgoing_" << code << "_summ"
            );
        }

        void SetTimeRanges(const TVector<TDuration>& bounds, NSv::TAuxData& aux) {
            TVector<double> histBounds;
            for (const auto& bound : bounds) {
                histBounds.push_back(bound.MicroSeconds() / SCALE);
            }

            ConnectTimes = &aux.CustomSignal<NSv::THistogram>(
                TStringBuilder{} << "report-" << Uuid << "-connect_times_hgram",
                histBounds
            );
            BackendTimes = &aux.CustomSignal<NSv::THistogram>(
                TStringBuilder{} << "report-" << Uuid << "-backend_times_hgram",
                histBounds
            );
            ProcessingTimes = &aux.CustomSignal<NSv::THistogram>(
                TStringBuilder{} << "report-" << Uuid << "-processing_times_hgram",
                histBounds
            );
        }
    };

    struct TReportStream : NSv::TStreamProxy {
    public:
        TReportStream(NSv::IStreamPtr s, const TAtomicSharedPtr<TReportStats> reportStats) noexcept
            : NSv::TStreamProxy(std::move(s))
            , ReportStats(std::move(reportStats))
            // , InitialStreamStats(*Stats())
            , EntranceTime(TInstant::Now())
        {}

        ~TReportStream() noexcept {
            // NSv::TStreamStats stats = GetStatsDelta(InitialStreamStats, *Stats());

            // Requests
            ++(*ReportStats->Requests);
            // Stats from stream
            // (*ReportStats->BackendAttempts) += stats.BackendAttempts;
            // (*ReportStats->BackendError) += stats.BackendError;
            // (*ReportStats->BackendTimeout) += stats.BackendTimeout;
            // (*ReportStats->ConnRefused) += stats.ConnRefused;
            // (*ReportStats->ConnTimeout) += stats.ConnTimeout;
            // (*ReportStats->ConnReset) += stats.ConnReset;
            // for (size_t i = 0; i < 5; ++i) {
            //     (*ReportStats->BackendStatusCodeCategs[i]) += stats.BackendStatusCodeCategs[i];
            // }

            // if (stats.ConnectTime != TDuration::Zero()) {
            //     if (ReportStats->ConnectTimes != nullptr) {
            //         ++(*ReportStats->ConnectTimes)[stats.ConnectTime.MicroSeconds() / SCALE];
            //     }
            // }
            // if (stats.FullBackendTime != TDuration::Zero()) {
            //     if (ReportStats->BackendTimes != nullptr) {
            //         ++(*ReportStats->BackendTimes)[stats.FullBackendTime.MicroSeconds() / SCALE];
            //     }
            // }
            if (ReportStats->ProcessingTimes != nullptr) {
                ++(*ReportStats->ProcessingTimes)[(TInstant::Now() - EntranceTime).MicroSeconds() / SCALE];
            }
        }

        bool WriteHead(NSv::THead& head) noexcept override {
            // NSv::TStreamStats stats = GetStatsDelta(InitialStreamStats, *Stats());

            // OutgoingCodeCategs
            if (100 <= head.Code && head.Code < 600) {
                ++(*ReportStats->OutgoingCodeCategs[head.Code / 100 - 1]);
            }
            // OutgoingCodes
            auto entry = ReportStats->OutgoingCodes.find(head.Code);
            if (entry != ReportStats->OutgoingCodes.end()) {
                ++(*entry->second);
            }

            // if (ReportStats->ConnectTimeHeader.Defined()) {
            //     if (stats.ConnectTime != TDuration::Zero()) {
            //         head.insert_or_replace({*ReportStats->ConnectTimeHeader, Retain(stats.ConnectTime.ToString())});
            //     }
            // }
            // if (ReportStats->BackendTimeHeader.Defined()) {
            //     if (stats.StreamStartTime != TInstant::Zero()) {
            //         TDuration backendHeadersTime = TInstant::Now() - stats.StreamStartTime;
            //         head.insert_or_replace({*ReportStats->BackendTimeHeader, Retain(backendHeadersTime.ToString())});
            //     }
            // }

            return NSv::TStreamProxy::WriteHead(head);
        }

    private:
        const TAtomicSharedPtr<TReportStats> ReportStats;
        // NSv::TStreamStats InitialStreamStats;
        TInstant EntranceTime;
    };

    const YAML::Node& reportNode = args["report"];
    CHECK_NODE(reportNode, reportNode.IsMap(), "report must be a map");

    TString uuid = NSv::Required<TString>(reportNode["uuid"]);
    TAtomicSharedPtr<TReportStats> stats = MakeAtomicShared<TReportStats>(uuid, aux);

    for (auto entry = reportNode.begin(); entry != reportNode.end(); ++entry) {
        if (entry->first.as<TStringBuf>() == "outgoing_codes") {
            CHECK_NODE(entry->second, entry->second.IsSequence(), "outgoing_codes must be a list");
            for (auto code : entry->second) {
                stats->AddOutgoingCode(code.as<int>(), aux);
            }
        } else if (entry->first.as<TStringBuf>() == "time_ranges") {
            CHECK_NODE(entry->second, entry->second.IsSequence(), "time_ranges must be a list");
            TVector<TDuration> bounds;
            for (auto bound : entry->second) {
                bounds.push_back(TDuration::Parse(bound.as<TStringBuf>()));
            }

            CHECK_NODE(entry->second, !bounds.empty(), "time_ranges must be non-empty");
            CHECK_NODE(entry->second, bounds.front() >= TDuration::Zero(), "time_ranges bounds must be non-negative");
            if (bounds.front() != TDuration::Zero()) {
                bounds.insert(bounds.begin(), TDuration::Zero());
            }
            for (size_t i = 0; i + 1 < bounds.size(); ++i) {
                CHECK_NODE(entry->second, bounds[i] < bounds[i + 1], "time_ranges bounds must ascend");
            }

            stats->SetTimeRanges(bounds, aux);
        } else if (entry->first.as<TStringBuf>() == "connect_time_header") {
            stats->ConnectTimeHeader = entry->second.as<TString>();
        } else if (entry->first.as<TStringBuf>() == "backend_time_header") {
            stats->BackendTimeHeader = entry->second.as<TString>();
        }
    }

    return [stats = std::move(stats)](NSv::IStreamPtr& s) {
        s = std::make_shared<TReportStream>(s, stats);
        return true;
    };
}

SV_DEFINE_ACTION("report", Report);
