#include "stream.h"

#include <apphost/lib/proto_answers/http.pb.h>

#include <util/generic/guid.h>
#include <util/random/random.h>
#include <util/string/ascii.h>
#include <util/string/builder.h>
#include <library/cpp/cgiparam/cgiparam.h>
#include <util/string/type.h>

static NSv::TAction AppHostHTTP(const YAML::Node&, NSv::TAuxData&) {
    return [json = true](NSv::IStreamPtr& req) {
        auto rqh = req->Head();
        if (!rqh MUN_RETHROW) {
            return false;
        }
        auto payload = NSv::ReadFrom(*req);
        if (!payload MUN_RETHROW) {
            return false;
        }
        TStringBuilder itemStr;
        if (json) {
            NJsonWriter::TBuf item{NJsonWriter::HEM_UNSAFE, &itemStr.Out};
            item.BeginObject();
            item.WriteKey("type").WriteString("http_request");
            item.WriteKey("remote_ip").WriteString(req->Peer().Format());
            // "port": this server's port?
            item.WriteKey("request_time").WriteULongLong(TInstant::Now().Seconds());
            item.WriteKey("method").WriteString(rqh->Method);
            item.WriteKey("uri").WriteString(rqh->PathWithQuery);
            item.WriteKey("headers").BeginList();
            for (const auto& hdr : *rqh)
                item.BeginList().WriteString(hdr.first).WriteString(hdr.second).EndList();
            item.EndList();
            item.WriteKey("content").WriteString(*payload);
            item.EndObject();
        } else {
            if (!EqualToOneOf(rqh->Method, "GET", "HEAD", "POST", "PUT", "DELETE")) {
                return req->WriteHead({405, {{"accept", "GET, POST, PUT, DELETE"}}}) && req->Close();
            }
            NAppHostHttp::THttpRequest item;
            item.SetMethod(rqh->Method == "POST"   ? NAppHostHttp::THttpRequest::Post
                         : rqh->Method == "PUT"    ? NAppHostHttp::THttpRequest::Put
                         : rqh->Method == "DELETE" ? NAppHostHttp::THttpRequest::Delete
                         : NAppHostHttp::THttpRequest::Get);
            for (const auto& h : *rqh) {
                auto target = item.AddHeaders();
                target->SetName(TString(h.first));
                target->SetValue(TString(h.second));
            }
            item.SetContent(std::move(*payload));
            itemStr << "p_" << item.SerializeAsString();
        }
        NJson::TJsonValue params;
        if (auto it = rqh->find("x-yandex-internal-request"); it != rqh->end() && ::IsTrue(it->second)) {
            for (const auto& [name, value] : TCgiParameters(rqh->Query())) {
                if (EqualToOneOf(name, "timeout", "dump", "mode", "reqid")) {
                    params[name] = value ? NJson::TJsonValue(value) : true;
                } else if (EqualToOneOf(name, "waitall")) {
                    params[name] = !value || ::IsTrue(value);
                } else if (EqualToOneOf(name, "srcskip")) {
                    params[name].AppendValue(value);
                } else if (EqualToOneOf(name, "srcrwr")) {
                    if (TStringBuf buf = value; TStringBuf source = buf.NextTok(':')) {
                        params[name][source] = buf;
                    }
                }
            }
        }
        auto wroteHead = std::make_shared<bool>(); // lol shared bool
        auto stream = NSv::NAppHost::Stream("/", false, {}, {},
            [req, wroteHead](const NSv::NAppHost::TResponse& chunk) {
                TVector<NJson::TJsonValue> items;
                for (const auto& answer : chunk.GetAnswers()) {
                    if (answer.GetType() == "http_response") {
                        NSv::NAppHost::DecodeJson(answer, [&](NJson::TJsonValue&& v) {
                            items.emplace_back(std::move(v));
                        });
                    }
                }
                if (items && !*wroteHead) {
                    NSv::THead rsh{(int)items[0]["status_code"].GetUIntegerRobust()};
                    if (rsh.Code == 0) {
                        rsh.Code = 200;
                    }
                    for (const auto& value : items) {
                        for (const auto& header : value["headers"].GetArray()) {
                            const auto& k = header[0].GetString();
                            const auto& v = header[1].GetString();
                            std::transform(k.begin(), k.end(), (char*)k.data(), [](char c) { return AsciiToLower(c); });
                            rsh.emplace(k, v);
                        }
                    }
                    if (!req->WriteHead(rsh) MUN_RETHROW) {
                        return false;
                    }
                    *wroteHead = true;
                }
                for (const auto& value : items) {
                    if (!req->Write(value["content"].GetString()) MUN_RETHROW) {
                        return false;
                    }
                }
                return true;
            },
            [req, wroteHead]() {
                return (*wroteHead || req->WriteHead(204)) && req->Close();
            },
            req->Log().Fork<NSv::NEv::TAppHostFromHTTP>());
        auto& ahReq = stream->GetBuffer();
        ahReq.SetGuid(CreateGuidAsString());
        ahReq.SetRuid(RandomNumber<ui64>());
        if (params.IsDefined()) {
            auto paramsItem = ahReq.AddAnswers();
            paramsItem->SetSourceName("APP_HOST_PARAMS");
            paramsItem->SetType("app_host_params");
            paramsItem->SetData(NAppHost::NCompression::Encode(ToString(params), NAppHost::NCompression::TCodecs::Default));
        }
        auto answer = ahReq.AddAnswers();
        answer->SetSourceName("HTTP_REQUEST");
        answer->SetType("http_request");
        answer->SetData(NAppHost::NCompression::Encode(itemStr, NAppHost::NCompression::TCodecs::Default));
        stream->CloseBuffer();
        req = std::move(stream);
        return true;
    };
}

static NSv::TAction AppHostHTTPBackend(const YAML::Node& args, NSv::TAuxData& aux) {
    return [proxy = aux.Action("proxy", args)](NSv::NAppHost::TInterface ah) {
        auto req = ah.ReadAll();
        if (!req MUN_RETHROW) {
            return false;
        }

        bool found = false;
        NAppHostHttp::THttpRequest hreq;
        NAppHostHttp::THttpResponse hrsp;
        for (const auto& answer : req->GetAnswers()) {
            if (answer.GetType() == "http_request") {
                auto data = NAppHost::NCompression::Decode(answer.GetData());
                auto view = TStringBuf(data);
                if (view.SkipPrefix("p_")) {
                    Y_PROTOBUF_SUPPRESS_NODISCARD hreq.ParseFromArray(view.data(), view.size());
                    found = true;
                    break;
                }
            }
        }
        if (!found) {
            return !mun_error(EINVAL, "no http_request protobuf item found");
        }

        TStringBuf method = hreq.GetMethod() == NAppHostHttp::THttpRequest::Post ? "POST"
                          : hreq.GetMethod() == NAppHostHttp::THttpRequest::Put ? "PUT"
                          : hreq.GetMethod() == NAppHostHttp::THttpRequest::Delete ? "DELETE" : "GET";
        TString path = hreq.GetPath().StartsWith("/") ? hreq.GetPath() : req->GetPath() + hreq.GetPath();
        NSv::THead head{method, path};
        for (auto& header : *hreq.MutableHeaders()) {
            header.MutableName()->to_lower();
            head.emplace(header.GetName() != "host" ? TStringBuf(header.GetName()) : ":authority", header.GetValue());
        }
        if (head.find(":scheme") == head.end()) {
            head.emplace(":scheme", "http");
        }
        if (head.find(":authority") == head.end()) {
            head.emplace(":authority", "unknown");
        }
        NSv::IStreamPtr fake = NSv::ConstRequestStream(head, hreq.GetContent(),
            [&](NSv::THead& rsh) {
                hrsp.SetStatusCode(rsh.Code);
                for (const auto& header : rsh) {
                    auto target = hrsp.AddHeaders();
                    target->SetName(TString(header.first));
                    target->SetValue(TString(header.second));
                }
                return true;
            },
            [&](TStringBuf chunk) {
                *hrsp.MutableContent() += chunk;
                return true;
            },
            [&](NSv::THeaderVector& trailers) {
                for (const auto& trailer : trailers) {
                    auto target = hrsp.AddHeaders();
                    target->SetName(TString(trailer.first));
                    target->SetValue(TString(trailer.second));
                }
                NSv::NAppHost::TResponse rsp;
                auto data = "p_" + hrsp.SerializeAsString();
                auto item = rsp.AddAnswers();
                item->SetType("http_response");
                item->SetData(NAppHost::NCompression::Encode(data, NAppHost::NCompression::TCodecs::Default));
                return ah.Write(std::move(rsp)) && ah.Close();
            },
            ah.Log().Fork<NSv::NEv::TAppHostToHTTP>(TString(head.Method), TString(head.Path()))
        );
        return proxy(fake);
    };
}

SV_DEFINE_ACTION("http-to-apphost", AppHostHTTP);
SV_DEFINE_ACTION("apphost-to-http", AppHostHTTPBackend);
