#include "transfer.h"

namespace NModProxy {
    namespace {

        void DebugLog(
            const TConnDescr& descr, TStringBuf logPrefix, TStringBuf message, const TChunkList& lst
        ) noexcept {
            if (NeedDebugLog(descr)) {
                Y_HTTPD_LOG_IMPL(
                    descr.ErrorLog, TLOG_DEBUG, "proxy", descr,
                    logPrefix << message << ' ' << lst.size()
                );
            }
        }

        void DebugLog(
            const TConnDescr& descr, TStringBuf logPrefix, TStringBuf message, TStringBuf str = {}
        ) noexcept {
            if (NeedDebugLog(descr)) {
                Y_HTTPD_LOG_IMPL(
                    descr.ErrorLog, TLOG_DEBUG, "proxy", descr,
                    logPrefix << message << str
                );
            }
        }
    }

    TTransfer::TTransfer(
        const TConnDescr& descr,
        IIoInput* i,
        IIoOutput* o,
        TDuration readTimeout,
        TDuration writeTimeout,
        TInstant sessionDeadline,
        TCoroSingleCondVar* finishCV,
        TSocketIo* readSocketIo,
        TString logPrefix,
        TCont* mainCont,
        bool watchReadSocket,
        bool isRequest
    ) noexcept
        : Descr_(descr)
        , I_(i)
        , O_(o)
        , ReadTimeout_(readTimeout)
        , WriteTimeout_(writeTimeout)
        , SessionDeadline_(sessionDeadline)
        , FinishCV_(finishCV)
        , ReadSocketIo_(readSocketIo)
        , LogPrefix_(std::move(logPrefix))
        , MainCont_(mainCont)
        , WatchReadSocket_(watchReadSocket)
        , IsRequest_(isRequest)
    {}

    void TTransfer::Finish() noexcept {
        Eof_ = true;
        if (IsRequest_ && Descr_.Request) {
            Descr_.Request->Props().TransferedWholeRequest = true;
        }
    }

    void TTransfer::Start(const char* name, TContExecutor* const exec) noexcept {
        TransferTask_ = TCoroutine{ECoroType::Service, name, exec, [this, exec] {
            Run(exec);
        }};
    }

    void TTransfer::Run(TContExecutor* const exec) {
        auto* const cont = exec->Running();
        Y_DEFER {
            FinishCV_->notify();
        };

        Y_TRY(TError, error) {
            uint8_t yieldCounter = 0;

            while (!Eof_ && !cont->Cancelled()) {
                ++yieldCounter; // Don't yield after first write because it causes redundant write syscall on chunked transfer
                if (yieldCounter % 8 == 0) {
                    cont->Yield();
                }

                DebugLog(Descr_, LogPrefix_, "reading next");

                TChunkList lst;
                Y_PROPAGATE_ERROR(DoRecv(lst));

                if (Eof_ = lst.Empty()) {
                    DebugLog(Descr_, LogPrefix_, "sending eof");
                } else {
                    DebugLog(Descr_, LogPrefix_, "sending next ", lst);
                }

                Y_PROPAGATE_ERROR(DoSend(std::move(lst)));
            }

            if (cont->Cancelled()) {
                DebugLog(Descr_, LogPrefix_, "exiting on cancel ");

                TSystemError err{ECANCELED};
                err << " TTransfer cont canceled";
                err << ' ' << cont->Name();
                ClientError_ = Y_MAKE_ERROR(std::move(err));
            } else {
                DebugLog(Descr_, LogPrefix_, "exiting on eof");
                Finish();
            }
            return {};
        } Y_CATCH {
            DebugLog(Descr_, LogPrefix_, "exiting on ", GetErrorMessage(error));
            if (auto* err = error.GetAs<TBackendError>()) {
                BackendError_ = std::move(error);
            } else {
                ClientError_ = std::move(error);
            }
        }
        WatchReadSocket();
    }

    TError TTransfer::DoRecv(TChunkList& lst) noexcept {
        TInstant readDeadline = Min(SessionDeadline_, ReadTimeout_.ToDeadLine());
        return I_->Recv(lst, readDeadline);
    }

    TError TTransfer::DoSend(TChunkList lst) noexcept {
        TInstant writeDeadline = Min(SessionDeadline_, WriteTimeout_.ToDeadLine());
        return O_->Send(std::move(lst), writeDeadline);
    }

    void TTransfer::WatchReadSocket() noexcept {
        if (!WatchReadSocket_ || !ReadSocketIo_) {
            return;
        }
        DebugLog(Descr_, LogPrefix_, "watch read socket");

        // If WaitRdhup didn't return a error then client has closed the connection and it's client error
        // WaitRdhup would return ECANCELED if transfer succeeded
        TError error = ReadSocketIo_->In().WaitRdhup();
        if (error) {
            if (const auto* e = error.GetAs<TSystemError>()) {
                if (e->Status() == ECANCELED) {
                    return;
                }
            }
        } else {
            error = Y_MAKE_ERROR(TSystemError{EIO} << "client left");
        }

        MainCont_->Cancel();
        DebugLog(Descr_, LogPrefix_, "watch read socket error ", GetErrorMessage(error));
        WatchReadSocketError_ = std::move(error);
    }
}
