#include "pessimization.h"
#include "updater.h"

#include <balancer/kernel/coro/coro_event.h>
#include <balancer/kernel/helpers/misc.h>
#include <util/stream/file.h>
#include <util/string/builder.h>
#include <util/system/fs.h>
#include <util/system/tempfile.h>


namespace NSrvKernel::NDynamicBalancing {

static const TVector<ui64> UpdateTimeIntervals = {
    0,
    250,
    500,
    750,
    1000,
    2500,
    5000,
    7500,
    10000,
    12500,
    15000,
    17500,
    20000,
    25000,
    50000,
    75000,
    100000,
    250000,
    500000,
    750000,
    1000000,
};

TBackendGroupUpdater::TBackendGroupUpdater(
    const TVector<TBackendDescriptor::TRef>& initialBackends,
    TConfig config, TSharedStatsManager& statsManager,
    TPingerStatsCounters* pingerStats,
    THotSwap<TBlacklist>& blacklist,
    TBackendGroupUpdater* old
)
    : Config_(std::move(config))
    , Log_(Config_.backends_name())
    , PessimizationTracker_(Config_, Log_)
    , OverPessimizedCount_(statsManager.MakeGauge("dynamic-over_pessimization").AllowDuplicate().Build())
    , UpdateTimes_(statsManager.MakeHistogram("dynamic-update_time", UpdateTimeIntervals).Scale(1e6).AllowDuplicate().Build())
    , PingerStats_(pingerStats)
    , CurrentBlacklist_(blacklist)
{
    Y_ENSURE_EX(0 <= Config_.max_pessimized_share() && Config_.max_pessimized_share() <= 1, TConfigParseError{} << "max_pessimized_share must be in [0, 1]");
    Y_ENSURE_EX(0 < Config_.weight_increase_step() && Config_.weight_increase_step() <= 1, TConfigParseError{} << "weight_increase_step must be in (0, 1]");
    Y_ENSURE_EX(0 < Config_.max_pessimization_steps(), TConfigParseError{} << "max_pessimization_steps must be positive");

    if (Config_.has_active()) {
        TPingerConfigUnparsed unparsedPingerConfig;
        unparsedPingerConfig.UnparsedRequest = Config_.active().request();
        unparsedPingerConfig.TcpCheck = Config_.active().tcp_check();
        unparsedPingerConfig.Delay = Config_.active().delay();
        unparsedPingerConfig.PingAfterConnError = Config_.active().ping_after_conn_error();

        if (!unparsedPingerConfig.UnparsedRequest && !unparsedPingerConfig.TcpCheck) {
            ythrow TConfigParseError{} << "You must enable tcp check or http check";
        }
        if (unparsedPingerConfig.UnparsedRequest && unparsedPingerConfig.TcpCheck) {
            ythrow TConfigParseError{} << "Can't use tcp check and http check simultaneously";
        }
        if (unparsedPingerConfig.TcpCheck && !Config_.active().has_use_backend_degraded_notification()) {
            Config_.active().set_use_backend_degraded_notification(false);
        }
        if (unparsedPingerConfig.TcpCheck && !Config_.active().has_use_backend_weight()) {
            Config_.active().set_use_backend_weight(false);
        }
        if (unparsedPingerConfig.TcpCheck && !Config_.active().has_use_backends_grouping()) {
            Config_.active().set_use_backends_grouping(false);
        }
        if (Config_.active().use_backend_weight() && unparsedPingerConfig.TcpCheck) {
            ythrow TConfigParseError{} << "Can't use backend dynamic weight with tcp check";
        }
        if (Config_.active().use_backend_degraded_notification() && unparsedPingerConfig.TcpCheck) {
            ythrow TConfigParseError{} << "Can't use backend degraded notification with tcp check";
        }
        if (Config_.active().use_backends_grouping() && unparsedPingerConfig.TcpCheck) {
            ythrow TConfigParseError{} << "Can't use backends grouping with tcp check";
        }
        if (unparsedPingerConfig.Delay == TDuration::Zero()) {
            ythrow TConfigParseError{} << "No ping delay configured";
        }

        if (!unparsedPingerConfig.UnparsedRequest.empty()) {
            unparsedPingerConfig.CheckRequest();
        }

        PingerConfig_ = TPingerConfig{unparsedPingerConfig};
    }

    if (!Config_.disable_defaults()) {
        if (!Config_.has_fail_rate_check()) {
            TFailRateCheckConfig config;
            config.set_min_requests(10);
            config.set_threshold(0.2);
            Config_.set_fail_rate_check(std::move(config));
        }

        if (!Config_.has_consecutive_fails_check()) {
            TConsecutiveFailsCheckConfig config;
            config.set_threshold(5);
            Config_.set_consecutive_fails_check(std::move(config));
        }
    }

    if (old) {
        with_lock(old->Mutex_) {
            PessimizationTracker_.DiscoverNewBackends(initialBackends, &old->PessimizationTracker_);
            old->Outdated_ = true;
        }
    } else {
        PessimizationTracker_.DiscoverNewBackends(initialBackends, nullptr);
    }
    UpdateNow();
}

TBackendGroupUpdater::~TBackendGroupUpdater() {
    if (WasOverPessimized_) {
        --OverPessimizedCount_;
    }
}

TCoroutine TBackendGroupUpdater::Start(IWorkerCtl* process) noexcept {
    Y_VERIFY(process->WorkerType() == NProcessCore::TChildProcessType::Updater);
    Y_VERIFY(!UpdaterProcess_);
    Y_ASSERT(process);

    UpdaterProcess_ = process;
    Log_.SetLog(UpdaterProcess_->GetDynamicBalancingLog());
    PessimizationTracker_.LogInitialInfo();

    if (PingerConfig_.Defined()) {
        // Create new pingers and terminate the old ones.
        // TODO(carzil): actually, we don't need recreate pingers in some cases.
        // TODO(carzil): really actually, we don't need separate pinger object, but only coroutine.
        Pingers_.clear();
        for (auto& entry : PessimizationTracker_.Backends()) {
            CreatePingerFor(entry.second->Clone());
        }
    }

    return TCoroutine{"updater_cont", &UpdaterProcess_->Executor(), [this]() {
        Run();
    }};
}

void TBackendGroupUpdater::UpdateNow() noexcept {
    if (Outdated_) {
        return;
    }
    TInstant updateStartedAt = TInstant::Now();
    Y_DEFER {
        UpdateTimes_.AddValue((TInstant::Now() - updateStartedAt).MicroSeconds());
    };

    PessimizationTracker_.UpdateBlacklist(GetCurrentBlacklist());
    PessimizationTracker_.UpdateNotReadySet(GetNotReadySet());

    auto backendsRef = PessimizationTracker_.Step();

    auto newState = MakeIntrusive<TBalancingState>(std::move(backendsRef));
    CurrentBalancingState_.AtomicStore(std::move(newState));

    if (PessimizationTracker_.IsOverPessimized() != WasOverPessimized_) {
        ToggleOverPessimizedMark();
    }
}

void TBackendGroupUpdater::ToggleOverPessimizedMark() noexcept {
    if (WasOverPessimized_) {
        --OverPessimizedCount_;
        WasOverPessimized_ = false;
    } else {
        ++OverPessimizedCount_;
        WasOverPessimized_ = true;
    }
}

void TBackendGroupUpdater::Run() noexcept {
    TDuration updateInterval = TDuration::Seconds(1);
    auto* const cont = UpdaterProcess_->Executor().Running();

    while (!cont->Cancelled()) {
        with_lock(Mutex_) {
            UpdateNow();
        }
        cont->SleepT((0.3 * RandomNumber<double>() + 1) * updateInterval);
    }
}

void TBackendGroupUpdater::CreatePingerFor(TIntrusivePtr<TDynamicBackend> backend) noexcept {
    Y_ASSERT(PingerConfig_.Defined());
    if (!PingerConfig_.Defined()) {
        return;
    }

    auto holder = MakeHolder<TPingerHolder>();
    auto& backendRef = *backend.Get();
    holder->Backend = std::move(backend);
    holder->Pinger = MakeHolder<TPingerV2>(backendRef, *PingerConfig_, *UpdaterProcess_,
                                           Config_.active().use_backend_weight(),
                                           Config_.active().use_backend_degraded_notification(),
                                           Config_.active().use_backends_grouping());
    holder->Pinger->LoadCounters(PingerStats_);
    auto* executor = &UpdaterProcess_->Executor();
    holder->Coroutine = TCoroutine{
        "backend_pinger", executor,
        [this, holder = holder.Get(), executor]() noexcept {
            auto* const cont = executor->Running();
            cont->SleepT(TDuration::Seconds(RandomNumber<double>() * holder->Pinger->Delay().SecondsFloat()));
            TBackendStatus backendStatus;
            while (!cont->Cancelled()) {
                const TDuration sleepTime = TDuration::Seconds((RandomNumber<double>() * 0.1 + 0.9) * holder->Pinger->Delay().Seconds());
                const TInstant nextRequest = sleepTime.ToDeadLine();

                bool ping = true;
                if (PingerConfig_->PingAfterConnError) {
                    ping = !IsBackendHealthy(*holder->Backend) || (!holder->Backend->ActiveChecked && holder->Backend->Status == TDynamicBackend::TStatus::NotChecked);
                    if (!ping) {
                        ping = holder->Backend->CurrentStats().ConnErrors > 0;
                    }
                }

                if (ping) {
                    backendStatus = holder->Pinger->Ping();
                    Log_ << "backend [" << holder->Backend->Name() << "]: health check "
                         << "is_alive=" << backendStatus.IsAlive << ", weight=" << backendStatus.Weight
                         << (backendStatus.IsWeightFromBackend ? " (from backend)" : " (default)")
                         << ", is_degraded=" << backendStatus.Degraded
                         << ", group=" << backendStatus.GroupName << Endl;
                    holder->Pinger->ProcessPingResult(backendStatus);
                    holder->Backend->ActiveChecked = true;
                }
                cont->SleepD(nextRequest);
            }
        }
    };
    Pingers_[backendRef.Name()] = std::move(holder);
}

void TBackendGroupUpdater::DumpBalancingState(NJson::TJsonWriter& out) const noexcept {
    PessimizationTracker_.Dump(out);
}


void TUpdaterManager::Start(IWorkerCtl* process) noexcept {
    if (BackendsBlacklistFile_.empty()) {
        return;
    }

    BlacklistReader_ = SharedFiles_->FileReReader(BackendsBlacklistFile_, TDuration::Seconds(1));
    TContExecutor* executor = &process->Executor();

    BlacklistFileUpdater_ = TCoroutine{"blacklist_updater_cont", executor, [this, executor, process]() {
        TDynamicLog log{"(blacklist_updater)"};
        log.SetLog(process->GetDynamicBalancingLog());

        size_t dataVersion = 0;
        auto* const cont = executor->Running();
        while (!cont->Cancelled()) {
            Y_DEFER {
                cont->SleepT(TDuration::Seconds(1));
            };

            if (dataVersion == BlacklistReader_.Data().Id()) {
                continue;
            }

            log << "new blacklist found" << Endl;

            auto blacklist = TBlacklist::Parse(BlacklistReader_.Data().Data());
            CurrentBlacklist_.AtomicStore(blacklist);
            dataVersion = BlacklistReader_.Data().Id();
        }
    }};
}

THolder<TBackendGroupUpdater> TUpdaterManager::RegisterBackends(
    size_t,
    const TVector<TBackendDescriptor::TRef>& backends,
    TConfig config,
    TBackendGroupUpdater* old
) {
    RequiresWorkerStart_ = true;
    return MakeHolder<TBackendGroupUpdater>(
        backends,
        std::move(config),
        SharedStatsManager_,
        PingerStats_,
        CurrentBlacklist_,
        old
    );
}

} // namespace NSrvKernel::NDynamicBalancing
