#include "ping_context.h"
#include "pinger.h"

using namespace NSrvKernel;
using namespace NBalancerClient;

bool NBalancerClient::IsCancelledError(const TError& error) {
    if (!error) {
        return false;
    }

    TError const* errorPtr = &error;

    if (auto* backendError = error.GetAs<NSrvKernel::TBackendError>()) {
        errorPtr = &backendError->InnerError();
    }

    return ErrorHasErrno(*errorPtr, {ECANCELED});
}

TPingContext::TPingContext(TOptions options)
    : Options_{std::move(options)}
    , PingAckReceivedEvent_{Options_.ContExecutor}
{
}

void TPingContext::Ping() {
    Coroutine_ = TCoroutine{ECoroType::Service, "ping_coroutine", Options_.ContExecutor, [this]() {
        Y_TRY(TError, error) {
            auto cancelTime = Options_.ContExecutor->Now() + Options_.PingTimeout;
            auto err = Options_.PingHandler(cancelTime);
            if (RunningCont()->Cancelled()) {
                return {};
            }

            if (err) {
                return Y_MAKE_ERROR(TBackendError{"ping send failed"});
            }

            SetState(EState::PingTimer);
            int res = PingAckReceivedEvent_.WaitD(cancelTime);
            if (res == ECANCELED) {
                return {};
            }

            if (res != EWAKEDUP) {
                return Y_MAKE_ERROR(TBackendError{"ping failed"});
            }

            if (Options_.MaxSuccessivePings > 0 && SuccessivePings_ >= Options_.MaxSuccessivePings) {
                SetState(EState::LimitReached);
                return {};
            }

            Options_.Pinger.Shedule(*this);
            return {};
        } Y_CATCH {
            SetState(EState::PingFailed);
            Options_.CancelHandler(std::move(error));
            return;
        };
        if (RunningCont()->Cancelled()) {
            SetState(EState::Cancelled);
        }
    }};
}

void TPingContext::DataReceived() {
    LastDataReceived_ = Options_.ContExecutor->Now();
    if (GetState() == EState::IdleTimer) {
        Options_.Pinger.Shedule(*this);
    }
}

void TPingContext::ResetSuccessivePings() {
    SuccessivePings_ = 0;
    if (GetState() == EState::LimitReached) {
        Options_.Pinger.Shedule(*this);
    }
}

void TPingContext::PingAck() {
    DataReceived();

    if (GetState() == EState::PingTimer) {
        SuccessivePings_++;
        PingAckReceivedEvent_.BroadCast();
    }
}

TPingContext::EState TPingContext::GetState() const {
    return State_;
}

size_t TPingContext::GetSuccessivePings() const {
    return SuccessivePings_;
}

void TPingContext::SetState(TPingContext::EState state) {
    if (state == State_) {
        return;
    }

    State_ = state;
    if (Options_.StateChangeHandler) {
        Options_.ContExecutor->CreateOwned([this, state](TCont*){
            Options_.StateChangeHandler(state);
        }, "TPingContext::SetState callback");
    }
}

void TPingContext::Cancel() {
    Unlink();
    if (Coroutine_.Running()) {
        Coroutine_.Cancel();
    } else {
        SetState(EState::Cancelled);
    }
}
