#pragma once

#include <balancer/serval/core/config.h>
#include <balancer/serval/core/http.h>
#include <balancer/serval/core/unistat.h>

#include <library/cpp/threading/hot_swap/hot_swap.h>

#include <util/generic/deque.h>
#include <util/generic/ymath.h>
#include <util/string/strip.h>
#include <util/system/thread.h>

namespace NSv {

    THead& RemoveConnectionHeaders(THead& head) noexcept;

    struct TConnEntry {
        // XXX fields are ordered so that destruction is done in correct order. *However*,
        //     this means move-assignment to a non-empty TConnEntry is not!
        TFile File;
        THolder<IO> IO;
        THolder<IConnection> Client;
        TThread::TId Thread = -1;
        cone::time CloseAt;
    };

    class TKeepAliveStack : TNonCopyable, TDeque<TConnEntry> {
    public:
        bool Pop(TConnEntry& out) noexcept;
        void Push(TConnEntry&& in, cone::timedelta timeout) noexcept;

    private:
        cone::mutex Lock;
        cone::guard Cleaner = [this]() noexcept {
            // Avoid excessive waking. The exactness of the timeout doesn't matter anyway.
            static constexpr auto period = std::chrono::seconds(10);
            for (auto next = cone::time::clock::now() + period; cone::sleep(next);) {
                while (true) {
                    TConnEntry conn;
                    auto now = cone::time::clock::now();
                    auto g = Lock.guard(cone::mutex::interruptible);
                    if (!g) {
                        return false;
                    }
                    if (!*this || (front().CloseAt > now && front().Client->IsOpen())) {
                        next = *this ? Min(Max(front().CloseAt, now + period / 2), now + period) : now + period;
                        break;
                    }
                    // Do not destroy it while holding the mutex, as the connection's destructor
                    // waits for its coroutines' termination. While the destructor itself runs
                    // in the current (admin) thread, the coroutines are pinned to workers and
                    // may take some time to clear the run queue when under heavy load.
                    conn = std::move(front());
                    pop_front();
                }
            }
            return false;
        };
    };

    struct TBackendOptions {
        THistogram* ConnTime = nullptr;
        THistogram* ExchangeTime = nullptr;
        std::array<TNumber<ui64>*, 5> StatusCodes = {};
        cone::timedelta ConnTimeout = std::chrono::milliseconds(25);
        cone::timedelta RecvTimeout = std::chrono::seconds(5);
        cone::timedelta SendTimeout = std::chrono::seconds(5);
        cone::timedelta Keepalive = std::chrono::seconds(30);
        TConnectionOptions Conn;
        TVector<std::pair<int /* code or -range */, bool /* is fast */>> RetryCodes;
        bool Sync = false;
    };

    bool Exchange(const TBackendOptions& opts, IStream& req, IConnection& conn, TLogFrame log);

    using TBackend = TFunction<bool(IStreamPtr&, const TBackendOptions&)>;

    TBackend Backend(URL url, IP ip, NSv::TNumber<ui64>* emptyWritesSignal);

    TBackend Backend(const YAML::Node& arg, TAuxData& aux);

    struct TWeightsVector : TAtomicRefCount<TWeightsVector>, TVector<float> {
        using TVector::TVector;
    };

    // TODO extract the reusable parts of this into core
    class TWeightsHolder : public THotSwap<TWeightsVector>, TNonCopyable {
    public:
        using THotSwap::THotSwap;

        TWeightsHolder(TString path, TVector<TString> names);

    private:
        bool Update();

    private:
        TString Path_;
        TVector<TString> Names_;
        cone::guard Updater_;
    };

    int ParseCode(const YAML::Node& node);

    TFunction<TMaybe<ui32>(const THead&)> ParseHashSpec(const YAML::Node& node);
}
