#include "resolver.h"

#include <balancer/kernel/thread/threadedqueue.h>

#include <util/generic/maybe.h>
#include <util/generic/ptr.h>
#include <util/network/socket.h>

#ifdef _win_
static const int errorResolutionErrorCode = WSASYSCALLFAILURE;
#else
static const int errorResolutionErrorCode = EAI_SYSTEM;
#endif

namespace NSrvKernel::NDns {

// ----------------------------------------------------------------------------
// TThreadedResolver
class TThreadedResolver : public TNonCopyable {
public:
    TThreadedResolver(TThreadedQueue* queue, TStatsCounters& counters, TDuration timeout) noexcept;

    TErrorOr<TSockAddrInfo> Resolve(const TResolveInfo& info, TInstant deadline = TInstant::Max()) noexcept;

private:
    class TImpl;
    const THolder<TImpl> Impl_;
    const TDuration Timeout_;
    TStatsCounters& Counters_;
};

class TThreadedResolver::TImpl : public TSimpleRefCount<TImpl> {
private:
    class TCallback : public TThreadedQueue::ICallback {
    public:
        explicit TCallback(TResolveInfo info) noexcept
            : ResolveInfo_(std::move(info))
            , Result_(Y_MAKE_ERROR(TNetworkResolutionError{errorResolutionErrorCode} << " address resolving failed or timouted"))
        {}

        TErrorOr<TSockAddrInfo>& Result() noexcept {
            return Result_;
        }

    private:
        void DoRun() override {
            if (Awaited()) {
                Result_ = TSimpleResolver::Resolve(ResolveInfo_);
            }
        }

    private:
        TResolveInfo ResolveInfo_;
        TErrorOr<TSockAddrInfo> Result_;
    };

public:
    explicit TImpl(TThreadedQueue* queue) noexcept
        : Queue_(queue)
    {
    }

    TErrorOr<TSockAddrInfo> Resolve(const TThreadedResolver* parent, TResolveInfo info, TInstant deadline) noexcept {
        const auto callback = Queue_->Run<TCallback>(new TCallback(std::move(info)),
                                                     Min(parent->Timeout_.ToDeadLine(), deadline));

        if (callback != nullptr) {
            return std::move(callback->Result());
        } else {
            return Y_MAKE_ERROR(
                    TNetworkResolutionError{errorResolutionErrorCode} << " address resolving failed or timouted");
        }
    }

private:
    TThreadedQueue* Queue_ = nullptr;
};

TThreadedResolver::TThreadedResolver(TThreadedQueue* queue, TStatsCounters& counters, TDuration timeout) noexcept
        : Impl_(MakeHolder<TImpl>(queue))
        , Timeout_(timeout)
        , Counters_(counters)
{
}

TErrorOr<TSockAddrInfo> TThreadedResolver::Resolve(const TResolveInfo& info, TInstant deadline) noexcept {
    if (info.Type == TResolveInfo::NUMERIC) {
        Counters_.DnsNumericSyncResolve.Add(1);
        return TSimpleResolver::Resolve(info);
    } else {
        Counters_.DnsResolve.Add(1);
        return Impl_->Resolve(this, info, deadline);
    }
}

// ----------------------------------------------------------------------------
// TContResolver::TAddressFuture
TContResolver::TAddressFuture::~TAddressFuture() = default;

TErrorOr<TSockAddrInfo> TContResolver::TAddressFuture::GetAddress(TInstant deadline, TStatsCounters& counters) noexcept  {
    if (!Task_.Running() && (ResultingAddress_ || DeadlinePassed())) {
        ResultingAddress_ = Y_MAKE_ERROR(yexception{} << "TContExecutor caching error");
        StartCont();
    }

    if (Task_.Running()) {
        int status = WaitEvent_.WaitD(deadline);
        if (status == ETIMEDOUT) {
            counters.DnsResolveTimeout.Add(1);
            return Y_MAKE_ERROR(TNetworkResolutionError{EAI_AGAIN} << "resolve timed out");
        } else if (status == ECANCELED) {
            counters.DnsResolveCancel.Add(1);
            return Y_MAKE_ERROR(yexception{} << "resolve canceled");
        }
    } else {
        counters.DnsCacheResolve.Add(1);
    }

    if (ResultingAddress_) {
        counters.DnsGeneralError.Add(1);
        return std::move(ResultingAddress_);
    }

    // ResultingAddress_ should store data for later calls. Logic of this DNS resolver should be altered as less
    // as possible in current commit. It is unsafe to return a pointer to resolver internal data.
    TSockAddrInfo addr;
    auto err = ResultingAddress_.AssignTo(addr);
    Y_ASSERT(!err);
    TSockAddrInfo result = addr;
    if (result.Addresses.empty()) {
        counters.DnsNoDataResult.Add(1);
    }
    ResultingAddress_ = std::move(addr);
    return std::move(result);
}

void TContResolver::TAddressFuture::StartCont() {
    Task_ = TCoroutine{ECoroType::Service, "network_address_resolver_cont", Executor_, [this] {
        ResultingAddress_ = Slave_->Resolve(Info_);
        if (!ResultingAddress_ && RecordTtl_ != TDuration::Max()) {
            TtlDeadline_ = RecordTtl_.ToDeadLine();
        }

        WaitEvent_.BroadCast();
    }};
}

// ----------------------------------------------------------------------------
// TContResolver
TContResolver::TContResolver(TContExecutor* executor, TStatsCounters& counters, TThreadedQueue* queue, TDuration timeout, TDuration recordTtl)
    : Executor_(executor)
    , Counters_(counters)
    , Slave_(std::make_unique<TThreadedResolver>(queue, counters, timeout))
    , RecordTtl_(recordTtl)
{
    Y_VERIFY(queue);
}

TContResolver::~TContResolver() = default;

}  // namespace NSrvKernel
