#include "http2_backend.h"

using namespace NSrvKernel;
using namespace NBalancerClient;

THttp2Backend::THttp2Backend(const TBackendConfig& config, TStateHolder* stateHolder)
    : Config_{config}
    , StateHolder_{stateHolder}
    , State_{stateHolder ? nullptr : std::make_shared<TState>()}
{
}

TErrorOr<THttp2Backend::TConnectionHolder> THttp2Backend::TState::GetOrCreateConnection(const TBackendConfig& config, const TConnDescr& descr, const THostInfo& hostInfo) noexcept {
    // В hostInfo может быть передан только Host или только CachedId, резолв имени происходит позже при установке соединения
    // В будущем стоит подумать над тем, чтобы разделить установку соединения и резолв домена
    auto host = hostInfo.CachedIp ? hostInfo.CachedIp: hostInfo.Host;
    TString addr = TStringBuilder{} << host << ":" << hostInfo.Port;

    auto& mutex = ConnectMutexes_.emplace(addr, MakeHolder<TContMutex>()).first->second;
    mutex->LockI(RunningCont());
    Y_DEFER {
        mutex->UnLock();
    };
    {
        auto connection = Connections_.find(addr);
        if (connection != Connections_.end() && connection->second->Alive()) {
            connection->second->Unlink();
            return TConnectionHolder{this, connection->second};
        }
    }

    TSimpleSharedPtr<THttp2Connection> connection;
    Y_PROPAGATE_ERROR(Connect(config, descr, hostInfo, addr).AssignTo(connection));

    Connections_[addr] = connection;

    return TConnectionHolder{this, connection};
}

TStringBuf THttp2Backend::ProtocolName() { return "http2_backend"; }

const TDuration& THttp2Backend::GetBackendTimeout() {
    return Config_.backend_timeout();
}

TErrorOr<THttp2Backend::TConnectionHolder> THttp2Backend::GetOrCreateConnection(const TConnDescr& descr, const THostInfo& hostInfo) noexcept {
    if (!State_) {
        State_ = StateHolder_->GetState(descr.Process().WorkerId());
    }

    return State_->GetOrCreateConnection(Config_, descr, hostInfo);
}

TErrorOr<TSimpleSharedPtr<THttp2Connection>> THttp2Backend::TState::Connect(const TBackendConfig& config, const TConnDescr& descr, const THostInfo& hostInfo, const TString& addr) noexcept
{
    TInstant start = Now();
    TDuration connectDuration = TDuration::Zero();
    TInstant effectiveSessionDeadline;
    THolder<NModProxy::TKeepAliveData> backendConnection;
    TInstant connectDeadline;

    Y_PROPAGATE_ERROR(NModProxy::EstablishConnection(descr, hostInfo, config, EPollMode::PM_EDGE_TRIGGERED, start, connectDuration, connectDeadline, effectiveSessionDeadline).AssignTo(backendConnection));

    auto connection = MakeSimpleShared<THttp2Connection>(addr, std::move(backendConnection), Pinger_);
    auto error = connection->Start(Min(config.http2_settings().wait_preface_timeout().ToDeadLine(), connectDeadline));
    if (error) {
        return Y_MAKE_ERROR(TConnectError(std::move(error), true));
    }

    return connection;
}

void THttp2Backend::TState::ReturnConnection(TSimpleSharedPtr<THttp2Connection> connection) {
    // Мертвое соединение нельзя переиспользовать
    if (!connection->Alive()) {

        // В Connections_ может лежать уже новое соединение
        auto connectionIt = Connections_.find(connection->Addr());
        if (connectionIt != Connections_.end() && connectionIt->second == connection) {
            Connections_.erase(connectionIt);
        }

        return;
    }

    // Живое соединение, если никем не используется, имеет только 2 ссылки
    // В connection и в Connections_
    if (connection.RefCount() > 2) {
        return;
    }

    UnusedConnections_.PushBack(connection.Get());

    if (CleanCoroutine_.Running()) {
        return;
    }

    CleanCoroutine_ = TCoroutine{ECoroType::Service, "http2 clean coroutine", RunningCont()->Executor(), std::mem_fn(&THttp2Backend::TState::Clean), this};
}

void THttp2Backend::TState::Clean() {
    while (!RunningCont()->Cancelled()) {
        if (UnusedConnections_.Empty()) {
            break;
        }

        auto* connection = UnusedConnections_.Front();
        if (!connection->Alive() || connection->IdleTime() > TDuration::Minutes(1)) {
            Y_ENSURE(Connections_.contains(connection->Addr()));
            Connections_.erase(connection->Addr());
            continue;
        }

        if (RunningCont()->SleepT(TDuration::Seconds(1)) != ETIMEDOUT) {
            break;
        }
    }

    while (UnusedConnections_) {
        Connections_.erase(UnusedConnections_.Front()->Addr());
    }
}

THttp2Backend::TState::TState(): Pinger_{{}} {}

std::shared_ptr<THttp2Backend::TState> THttp2Backend::TStateHolder::GetState(size_t workerId) {
    auto state = States_[workerId].lock();
    if (!state) {
        state = std::make_shared<TState>();
        States_[workerId] = state;
    }
    return state;
}

THttp2Backend::TStateHolder::TStateHolder(size_t workersCount): States_{workersCount} {}

THttp2Backend::TState::TConnectionHolder::TConnectionHolder(TState* state, TSimpleSharedPtr<THttp2Connection> connection)
    : State_{state}
    , Connection_{std::move(connection)}
{}

THttp2Connection* THttp2Backend::TState::TConnectionHolder::operator->() {
    return Connection_.Get();
}

THttp2Backend::TState::TConnectionHolder::~TConnectionHolder() {
    if (Connection_) {
        State_->ReturnConnection(std::move(Connection_));
    }
}
