#include "dynamic_backend.h"

namespace {
    static bool IsTimeout(const NSrvKernel::TError& error) {
        if (auto* err = error.GetAs<NSrvKernel::TBackendError>()) {
            if (auto* sysErr = err->InnerError().GetAs<TSystemError>()) {
                if (sysErr->Status() == ETIMEDOUT) {
                    return true;
                }
            }
        }
        return false;
    }
}

using namespace NSrvKernel::NDynamicBalancing;

TDynamicBackendStatsStorage::TDynamicBackendStatsStorage(ui64 minRequests, TDuration historyInterval)
    : MinRequests_(minRequests)
    , ChunkMinDuration_(historyInterval / CHUNKS) {
    TInstant now = TInstant::Now();
    for (auto& chunk : Chunks_) {
        chunk.StartedFrom = now;
    }
    Total_.StartedFrom = now;
}

TDynamicBackendStats TDynamicBackendStatsStorage::CurrentStats() const noexcept {
    TDynamicBackendStats copy;
    with_lock(Mutex_) {
        copy = Total_;
    }
    return copy;
}

size_t TDynamicBackendStatsStorage::Tail(size_t offset) const noexcept {
    return (Index_ + 1 + offset) % CHUNKS;
}

void TDynamicBackendStatsStorage::AddRequest(const TError& error, const TDuration& duration) noexcept {
    ui64 processingTime = duration.MilliSeconds();
    TInstant now = TInstant::Now();
    with_lock(Mutex_) {
        if (Total_.Requests + 1 - Chunks_[Tail()].Requests >= MinRequests_ && Chunks_[Index_].StartedFrom + ChunkMinDuration_ <= now) {
            IncIndex(now);
        }
        TDynamicBackendStats& current = Chunks_[Index_];
        current.Requests += 1;
        Total_.Requests += 1;
        current.SumProcessingTime += processingTime;
        Total_.SumProcessingTime += processingTime;
        if (error) {
            if (error.GetAs<NSrvKernel::TConnectError>()) {
                current.ConnErrors += 1;
                Total_.ConnErrors += 1;
            }
            if (IsTimeout(error)) {
                current.Timeouts += 1;
                Total_.Timeouts += 1;
            } else {
                current.Fails += 1;
                Total_.Fails += 1;
            }
        }
    }
}

void TDynamicBackendStatsStorage::IncIndex(TInstant start) noexcept {
    if (++Index_ == CHUNKS) {
        Index_ = 0;
    }
    TDynamicBackendStats& toReset = Chunks_[Index_];
    Total_.Requests -= toReset.Requests;
    toReset.Requests = 0;
    Total_.SumProcessingTime -= toReset.SumProcessingTime;
    toReset.SumProcessingTime = 0;
    Total_.Fails -= toReset.Fails;
    toReset.Fails = 0;
    Total_.Timeouts -= toReset.Timeouts;
    toReset.Timeouts = 0;
    Total_.ConnErrors -= toReset.ConnErrors;
    toReset.ConnErrors = 0;
    Total_.StartedFrom = Chunks_[Tail()].StartedFrom;
    toReset.StartedFrom = start;
}

void TDynamicBackendStatsStorage::DropOldStats() noexcept {
    TInstant now = TInstant::Now();
    TInstant windowStart = now - ChunkMinDuration_ * CHUNKS;
    with_lock(Mutex_) {
        while (Total_.Requests >= MinRequests_ && Chunks_[Tail(1)].StartedFrom <= windowStart) {
            IncIndex(now);
        }
    }
}

void TDynamicBackend::OnFailRequest(const TError& error, const TDuration& duration) noexcept {
    Stats_.AddRequest(error, duration);

    TrackedConsecutiveRequests.fetch_add(1);
    auto consecutiveFails = ConsecutiveFails.fetch_add(1) + 1;
    auto initialMax = ConsecutiveFailsMax.load();
    for (;;) {
        auto max = ConsecutiveFailsMax.load();
        if (max < initialMax || max >= consecutiveFails || ConsecutiveFailsMax.compare_exchange_strong(max, consecutiveFails)) {
            break;
        }
    }
}

void TDynamicBackend::OnCompleteRequest(const TDuration& duration) noexcept {
    Stats_.AddRequest({}, duration);

    ConsecutiveFails.store(0);
    TrackedConsecutiveRequests.fetch_add(1);
}

void TDynamicBackend::ResetConsecutiveFails() noexcept {
    ConsecutiveFails.store(0);
    ConsecutiveFailsMax.store(0);
    TrackedConsecutiveRequests.store(0);
}

void TDynamicBackend::SetWeightFromPing(double value, bool valueFromBackend) noexcept {
    if (valueFromBackend) {
        value /= WeightNormalizationCoeff_;
    }
    TPerWorkerBaseBackend::SetWeightFromPing(value, valueFromBackend);
}

void TDynamicBackend::SetWeightNormalizationCoeff(double value) noexcept {
    if (value) {
        WeightNormalizationCoeff_ = value;
    }
}

void TDynamicBackend::Dump(NJson::TJsonWriter& out, bool extended) const noexcept {
    out.Write("name", Name());
    out.Write("status", ToString(Status.load()));
    out.Write("pessimized", Pessimized);
    out.Write("final_weight", FinalWeight());
    out.Write("group", GroupName());

    if (!extended) {
        return;
    }

    out.Write("weight_coeff", WeightCoeff);
    out.Write("target_weight_coeff", TargetWeightCoeff);
    out.Write("pessimization_steps_remaining", PessimizationStepsRemaining);
    out.Write("current_pessimization_interval", CurrentPessimizationInterval);

    auto stats = CurrentStats();
    out.OpenMap("stats");
    out.Write("requests", stats.Requests);
    out.Write("fails", stats.Fails);
    out.Write("timeouts", stats.Timeouts);
    out.Write("conn_errors", stats.ConnErrors);
    out.Write("spt", stats.SumProcessingTime);
    out.Write("since", stats.StartedFrom.ToStringLocal());
    out.Write("consecutive_fails", ConsecutiveFails.load(std::memory_order_relaxed));
    out.Write("consecutive_fails_max", ConsecutiveFailsMax.load(std::memory_order_relaxed));
    out.Write("consecutive_tracked", TrackedConsecutiveRequests.load(std::memory_order_relaxed));
    out.Write("selections", Selections.load(std::memory_order_relaxed));
    out.Write("expectations", Expectations.load(std::memory_order_relaxed));
    out.CloseMap();
}

TBalancingState::TBalancingState(TVector<TIntrusivePtr<TDynamicBackend>> balancingState)
    : Backends_(std::move(balancingState))
{
    const size_t size = Backends_.size();
    Snapshots_.reserve(size);
    AllWeights_.BoundaryAndIndex.reserve(size);
    for (size_t i = 0; i < size; ++i) {
        const auto& backend = Backends_[i];
        const double weight = Max(backend->FinalWeight(), 0.0);
        AllWeights_.Add(weight, i);
        TString groupName = backend->GroupName();
        if (groupName) {
            Groups_[groupName].Add(weight, i);
        }
        Snapshots_[backend.Get()] = {weight, std::move(groupName)};
    }
    Y_VERIFY(Snapshots_.size() == Backends_.size());
    Y_VERIFY(AllWeights_.BoundaryAndIndex.size() == Backends_.size());
}

void TBalancingState::Dump(NJson::TJsonWriter& out) const noexcept {
    out.OpenArray("backends");
    for (const auto& backend : Backends_) {
        out.OpenMap();
        backend->PrintProxyInfo(out);
        backend->Dump(out, false);
        out.CloseMap();
    }
    out.CloseArray();
}
