#include "backends.h"
#include <balancer/kernel/coro/coroutine.h>
#include <balancer/kernel/custom_io/null.h>
#include <balancer/kernel/process/config_check.h>
#include <balancer/kernel/requester/requester.h>
#include <util/random/shuffle.h>

namespace NSrvKernel {
    TBackendsUID::TValue NextUID = 0;

    TBackendsUID::TBackendsUID()
        : Value(++NextUID)
    {
    }

    TError IBackends::CheckBackend(IWorkerCtl& proc, TBackendDescriptor::TRef backend, const TMaybe<TRequest>& request) const noexcept {
        TAsyncRequester requester(*backend->Module(), nullptr, proc);
        requester.Props().SkipKeepalive = true;
        if (request) {
            TResponse response;
            Y_PROPAGATE_ERROR(requester.Requester().Request(TRequest{*request}, response));
            if (response.ResponseLine().StatusCode == HTTP_OK) {
                return {};
            }
            return Y_MAKE_ERROR(yexception{} << "active check returned " << response.ResponseLine().StatusCode);
        }
        TNullStream in;
        TNullStream out;
        return requester.Requester().TcpRequest(in, out);
    }

    TBackendCheckResult IBackends::VisitBackends(
        IWorkerCtl& proc, bool runtimeCheck,
        std::function<void(IWorkerCtl&, TBackendDescriptor::TRef, TBackendCheckResult&, bool)> func) const noexcept {
        if (!BackendDescriptors()) {
            return TBackendCheckResult{MakeError<TNoValidBackendsError>(), TBackendCheckResult::EStatus::Failed};
        }

        TBackendCheckResult result{TBackendCheckResult::EStatus::Success};
        if (runtimeCheck) {
            for (auto backend : BackendDescriptors()) {
                func(proc, backend, result, true);
            }
            return result;
        }

        auto bds = BackendDescriptors();
        Shuffle(bds.begin(), bds.end());

        TVector<TCoroutine> coroutines;
        for (auto i : xrange(bds.size())) {
            coroutines.emplace_back(TCoroutine("backend_check", &proc.Executor(), [&proc, i, &func, &result](TBackendDescriptor::TRef backend) {
                proc.Executor().Running()->SleepT(TDuration::MicroSeconds(100 * i));
                func(proc, backend, result, false);
            }, bds[i]));
        }

        for (auto& coro : coroutines) {
            coro.Join();
        }
        return result;
    }

    TBackendCheckParameters IBackends::ActualCheckParameters(IWorkerCtl& proc) const noexcept {
        TBackendCheckParameters overridenParameters;
        const auto& overrides = proc.BackendCheckOverrides();
        if (auto ptr = overrides.Sections.FindPtr(CheckParameters_.SectionName)) {
            overridenParameters = *ptr;
        } else if (overrides.Global) {
            overridenParameters = *overrides.Global;
        } else {
            return CheckParameters_;
        }

        if (!overridenParameters.Quorum) {
            overridenParameters.Quorum = CheckParameters_.Quorum;
        }
        if (!overridenParameters.AmountQuorum) {
            overridenParameters.AmountQuorum = CheckParameters_.AmountQuorum;
        }
        if (!overridenParameters.Hysteresis) {
            overridenParameters.Hysteresis = CheckParameters_.Hysteresis;
        }
        if (!overridenParameters.AmountHysteresis) {
            overridenParameters.AmountHysteresis = CheckParameters_.AmountHysteresis;
        }

        return overridenParameters;
    }

    TBackendCheckResult IBackends::CheckBackends(IWorkerCtl& proc, bool runtimeCheck) noexcept {
        TBackendCheckParameters parameters = ActualCheckParameters(proc);
        if (ShouldSkipCheck(parameters) || runtimeCheck) {
            return TBackendCheckResult{TBackendCheckResult::EStatus::Skipped};
        }
        TBackendCheckResult result = VisitBackends(proc, runtimeCheck, [this](IWorkerCtl& proc, TBackendDescriptor::TRef backend, TBackendCheckResult& result, bool){
            if (TError err = CheckBackend(proc, backend)) {
                result.Errors.emplace_back(std::move(err));
            }
        });
        PostProcessCheckResult(result, parameters, BackendDescriptors().size(), result.Errors.size());
        return result;
    }

    bool IBackends::ShouldSkipCheck(const TBackendCheckParameters& parameters) const noexcept {
        if (parameters.Quorum && *parameters.Quorum > 0) {
            return false;
        }
        if (parameters.AmountQuorum && *parameters.AmountQuorum > 0) {
            return false;
        }
        return true;
    }

    void IBackends::PostProcessCheckResult(TBackendCheckResult& result, const TBackendCheckParameters& parameters, size_t totalCount, size_t failuresCount) noexcept {
        if (result.Status != TBackendCheckResult::EStatus::Failed) {
            if (parameters.Quorum) {
                double quorum = Min(*parameters.Quorum, 1.0);
                if (parameters.Hysteresis) {
                    if (LastCheckStatus_ == TBackendCheckResult::EStatus::Failed) {
                        quorum = Min(quorum + *parameters.Hysteresis, 1.0);
                    } else if(LastCheckStatus_ == TBackendCheckResult::EStatus::Success) {
                        quorum = Max(quorum - *parameters.Hysteresis, 0.0);
                    }
                }
                if (totalCount < failuresCount + totalCount * quorum) {
                    result.Status = TBackendCheckResult::EStatus::Failed;
                }
            }
            if (parameters.AmountQuorum) {
                size_t amountQuorum = *parameters.AmountQuorum;
                if (parameters.AmountHysteresis) {
                    if (LastCheckStatus_ == TBackendCheckResult::EStatus::Failed) {
                        amountQuorum += *parameters.AmountHysteresis;
                    } else if(LastCheckStatus_ == TBackendCheckResult::EStatus::Success) {
                        amountQuorum -= Min(*parameters.AmountHysteresis, amountQuorum);
                    }
                }
                if (totalCount < failuresCount + amountQuorum) {
                    result.Status = TBackendCheckResult::EStatus::Failed;
                }
            }
        }
        if (result.Status != TBackendCheckResult::EStatus::Skipped) {
            LastCheckStatus_ = result.Status;
        }
    }

}  // namespace NSrvKernel
