#include "http2_stream.h"
#include "http2_connection.h"

#include <util/string/ascii.h>

using namespace NBalancerClient;
using namespace NSrvKernel;

namespace {
const TStringBuf STATUS = ":status";
}

ssize_t THttp2Stream::CheckNewData(ui8 *buf, size_t length, ui32 *data_flags) {
    Y_UNUSED(buf);

    if (Result_) {
        return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
    }

    if (!HaveNewChunk_) {
        Deferred_ = true;
        ReadEvent_.BroadCast();
        return NGHTTP2_ERR_DEFERRED;
    }

    *data_flags |= NGHTTP2_DATA_FLAG_NO_COPY;
    if (!ClientInput_.HasBufferedData()) {
        *data_flags |= NGHTTP2_DATA_FLAG_EOF;
    }

    return Min(ClientInput_.BytesBuffered(), length);
}

NSrvKernel::TErrorOr<NSrvKernel::TChunkList> THttp2Stream::GetNewData(size_t length) {
    TChunkList data;
    if (auto error = ClientInput_.Next(length, data, TInstant::Zero())) {
        Result_ = error.Clone();
        DoneEvent_.BroadCast();
        return error;
    }

    if (!data.Empty()) {
        if (!ClientInput_.HasBufferedData()) {
            if (Callbacks_.OnChunkSent) {
                Callbacks_.OnChunkSent(ChunkSize_);
            }
            HaveNewChunk_ = false;
        }
    } else {
        if (Callbacks_.OnEndOfInputStream) {
            Callbacks_.OnEndOfInputStream();
        }
    }

    return data;
}

int THttp2Stream::OnFrameRecv(const nghttp2_frame *frame) {
    if (Callbacks_.OnFrameRecvHandler) {
        Callbacks_.OnFrameRecvHandler(frame);
    }

    if (Result_) {
        return 0;
    }

    if (frame->hd.flags & NGHTTP2_FLAG_END_HEADERS) {
        if (!HeadersReceived_) {
            HeadersReceived_ = true;

            if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
                Response_.Props().ContentLength = 0;
            }

            TError error = ClientOutput_.SendHead(std::move(Response_), false, TInstant::Zero());
            if (error) {
                Result_ = std::move(error);
                DoneEvent_.BroadCast();
                return 0;
            }
        }
    }
    if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
        TError error;
        if (Trailers_.Size() > 0) {
            error = ClientOutput_.SendTrailers(std::move(Trailers_), TInstant::Zero());
            if (error) {
                Result_ = std::move(error);
                DoneEvent_.BroadCast();
                return 0;
            }
        }
        error = ClientOutput_.SendEof(TInstant::Zero());
        if (error) {
            Result_ = std::move(error);
            DoneEvent_.BroadCast();
            return 0;
        }
    }

    return 0;
}

int THttp2Stream::OnFrameSent(const nghttp2_frame* frame) {
    if (Callbacks_.OnFrameSentHandler) {
        Callbacks_.OnFrameSentHandler(frame);
    }

    return 0;
}

int THttp2Stream::OnData(ui8 /*flags*/, const ui8 *data, size_t len) {
    if (Result_) {
        return 0;
    }

    if (Callbacks_.OnChunkReceived) {
        Callbacks_.OnChunkReceived(len);
    }

    TChunkList chunkList;
    chunkList.PushNonOwning(TStringBuf{reinterpret_cast<const char*>(data), len});
    auto error = ClientOutput_.Send(std::move(chunkList), TInstant::Zero());
    if (error) {
        Result_ = std::move(error);
        DoneEvent_.BroadCast();
        return 0;
    }

    return 0;
}

void THttp2Stream::OnStreamClose(TError error) {
    if (!Result_) {
        Result_ = std::move(error);
        DoneEvent_.BroadCast();
    }
    StreamClosed_ = true;
    Unlink();
}

int THttp2Stream::OnHeader(const nghttp2_frame* /*frame*/, nghttp2_rcbuf *nameRcBuf, nghttp2_rcbuf *valueRcBuf, ui8 /*flags*/) {
    auto nameBuf = nghttp2_rcbuf_get_buf(nameRcBuf);
    auto valueBuf = nghttp2_rcbuf_get_buf(valueRcBuf);
    // TODO: копирование данных
    TString name{reinterpret_cast<char*>(nameBuf.base), nameBuf.len};
    TString value{reinterpret_cast<char*>(valueBuf.base), valueBuf.len};

    if (HeadersReceived_) {
        Trailers_.Add(name, value);
        return 0;
    }

    if (name.StartsWith(':')) {
        if (name == STATUS) {
            Response_.ResponseLine().StatusCode = FromString<ui16>(value);
        }
        return 0;
    }
    Response_.Headers().Add(name, value);
    return 0;
}

NSrvKernel::TError THttp2Stream::Join(TInstant deadline) {
    if (Result_) {
        return std::move(*Result_);
    }

    int status = DoneEvent_.WaitD(deadline);

    Y_REQUIRE(status == EWAKEDUP, TSystemError{status} << "join failed");
    return std::move(*Result_);
}

THttp2Stream::THttp2Stream(THttp2Connection& connection,
                           i32 streamId,
                           TCont* cont,
                           IIoInput& input,
                           IHttpOutput& output,
                           TCallbacks callbacks)
    : Connection_{connection}
    , StreamId_{streamId}
    , ClientInput_{&input}
    , ClientOutput_{output}
    , ReadEvent_{cont->Executor()}
    , DoneEvent_{cont->Executor()}
    , ReadCoroutine_{ECoroType::Service, "read input coroutine", cont->Executor(), std::mem_fn(&THttp2Stream::Read), this}
    , Callbacks_{std::move(callbacks)}
{
}

THttp2Stream::~THttp2Stream() {
    if (!StreamClosed_) {
        Y_UNUSED(Connection_.CancelStream(*this, TInstant::Zero()));
    }
    Y_VERIFY(!Connection_.HasStream(StreamId_));
}

i32 THttp2Stream::GetStreamId() const {
    return StreamId_;
}

void THttp2Stream::Read() {
    for(;;) {
        TChunkList chunkList;
        TError error = ClientInput_.Next(chunkList, TInstant::Max());
        if (Result_) {
            return;
        }
        if (error) {
            Result_ = std::move(error);
            DoneEvent_.BroadCast();
            return;
        }

        ChunkSize_ = chunkList.size();
        ClientInput_.UnGet(chunkList);

        HaveNewChunk_ = true;
        bool resume = Deferred_;
        Deferred_ = false;
        error = Connection_.StreamHasNewData(*this, resume);
        if (error) {
            Result_ = std::move(error);
            DoneEvent_.BroadCast();
            return;
        }

        if (ChunkSize_ == 0) {
            break;
        }

        if (Deferred_) {
            continue;
        }

        if (ReadEvent_.WaitD(TInstant::Max()) != EWAKEDUP) {
            break;
        }
    }
}
