#include "client.h"

#include <saas/util/json/json.h>

#include <library/cpp/dns/cache.h>
#include <library/cpp/json/json_reader.h>
#include <library/cpp/http/io/stream.h>
#include <library/cpp/http/multipart/send.h>

#include <util/string/vector.h>
#include <library/cpp/cgiparam/cgiparam.h>

using namespace NSaas;
namespace {
    const TString JSON_TYPE = "json";
    const TString JSON_REF_TYPE = "json_ref";
    const TString PROTO_TYPE = "proto";
    const TString DEBUG_SUFIX = "_debug";
}

class TJsonSendResultImpl: public TSendResult::IImpl {
public:
    TJsonSendResultImpl(int code, const TString& replyBody)
        : ProxyCode(code)
        , ProxyReply(replyBody)
    {
        SendResult = InterpretHTTPCode(ProxyCode);
        FillAsyncCode(ProxyReply);
    }

    TString GetMessage() const override {
        return ProxyReply;
    }
    TSendResult::ESource GetSource() const override {
        return TSendResult::PROXY;
    }
    bool IsAsync() const override {
        return !AsyncCode.empty();
    }
    TString GetAsyncHandle() const override {
        return AsyncCode;
    }
private:
    TSendResult::ESendResult InterpretHTTPCode(ui16 httpCode) const {
        switch (httpCode) {
        case 200: return TSendResult::srOK;
        case 202: return TSendResult::srDataAccepted;
        case 206: return TSendResult::srPartial;
        case 503: return TSendResult::srNotNow;
        case 304: return TSendResult::srDeprecated;
        case 400: return TSendResult::srIncorrectMessage;
        case 598: return TSendResult::srSendError;
        case 401: return TSendResult::srUnauthorized;
        case 512: return TSendResult::srDifferentAnswers;
        case 513: return TSendResult::srCantStore;
        case 514: return TSendResult::srInternalTimeout;
        }
        if (httpCode >= 500)
            return TSendResult::srServerError;
        return TSendResult::srUnknownError;
    }
    void FillAsyncCode(const TString& response) {
        NJson::TJsonValue json;
        if (response && NUtil::JsonFromString(response, json)) {
            AsyncCode = json["async_code"].GetString();
        }
    }
private:
    ui16 ProxyCode;
    TString ProxyReply;
    TString AsyncCode;
};

class TFakeSendResultImpl: public TSendResult::IImpl {
private:
    TString Message;

public:
    TFakeSendResultImpl(TSendResult::ESendResult result = TSendResult::srUnknownError)
    {
        SendResult = result;
    }

    TFakeSendResultImpl(TString message, TSendResult::ESendResult result = TSendResult::srUnknownError)
        : Message(std::move(message))
    {
        SendResult = result;
    }

    TString GetMessage() const override {
        return Message;
    }
    TSendResult::ESource GetSource() const override {
        return TSendResult::ARTIFICAL;
    }
    bool IsAsync() const override {
        return false;
    }
    TString GetAsyncHandle() const override {
        return {};
    }
};

TIndexingClient::TIndexingClient(const TString& host, ui16 port, const TString& url, const TDuration& timeout)
    : Host(host)
    , Port(port)
    , Url(url)
    , Timeout(timeout)
{}

TSendResult TIndexingClient::Send(const TAction& action, const TString& type, const TSendParams& params) const {

    if (type.EndsWith(DEBUG_SUFIX)) {
        TString trueType = type.substr(0, type.length() - DEBUG_SUFIX.length());
        Cout << GetHttpMessage(action, trueType, params) << Endl;
        return TSendResult(TSendResult::srOK);
    }
    TString data = GetHttpMessage(action, type, params);
    try {
        DEBUG_LOG << "sending " << action.GetActionDescription() << Endl;
        return SendToProxy(data.data(), data.size());
    }
    catch (...) {
        ERROR_LOG << "Exception while sending " << action.GetActionDescription() << " " << type << " " << CurrentExceptionMessage() << Endl;
        return TSendResult::FromCurrentExeption();
    }
}

ui16 NSaas::TSendResult::InterpretSendResult(ESendResult result) const {
    switch (result) {
    case NSaas::TSendResult::srOK: return 200;
    case NSaas::TSendResult::srIncorrectMessage: return 400;
    case NSaas::TSendResult::srNotNow: return 302;
    case NSaas::TSendResult::srDataAccepted: return 202;
    case NSaas::TSendResult::srServerError:
    case NSaas::TSendResult::srUnknownError: return 500;
    case NSaas::TSendResult::srDeprecated: return 304;
    case NSaas::TSendResult::srSendError: return 598;
    case NSaas::TSendResult::srUnauthorized: return 401;
    case NSaas::TSendResult::srCantStore: return 513;
    case NSaas::TSendResult::srDifferentAnswers: return 512;
    case NSaas::TSendResult::srPartial: return 206;
    case NSaas::TSendResult::srInternalTimeout: return 514;
    default: return 0;
    }
}

bool TSendResult::ShouldRetryMessage() const {
    auto result = GetCode();
    return result == srSendError ||
           result == srNotNow ||
           result == srDataAccepted;
}

bool NSaas::TSendResult::IsSucceeded() const {
    return GetCode() == srOK || GetCode() == srPartial;
}

NSaas::TSendResult NSaas::TSendResult::FromProxyReply(int code, const TString& body) {
    TSendResult result;
    result.Impl.Reset(MakeSimpleShared<TJsonSendResultImpl>(code, body));
    return result;
}

NSaas::TSendResult NSaas::TSendResult::FromProxyReply(const TString& head, const TString& body) {
    return FromProxyReply(ParseHttpRetCode(head), body);
}

NSaas::TSendResult NSaas::TSendResult::FromCurrentExeption(TSendResult::ESendResult eSendResult) {
    TSendResult result;
    result.Impl.Reset(MakeSimpleShared<TFakeSendResultImpl>(CurrentExceptionMessage(), eSendResult));
    return result;
}

TVector<std::pair<ui16, TString>> NSaas::TSendResult::GetReplies() const {
    TVector<std::pair<ui16, TString>> result;
    result.push_back(std::make_pair(GetHttpCode(), GetMessage()));
    return result;
}

NSaas::TSendResult::TSendResult(ESendResult result)
    : Impl(MakeSimpleShared<TFakeSendResultImpl>(result))
{}

bool NSaas::TIndexingClient::WaitAsync(TSendResult& result, TInstant deadline) const {
    if (!result.IsAsync())
        return true;

    if (result.GetSource() == TSendResult::PROXY) {
        TSendParams params;
        params.Trace = true;
        const TString& asyncCode = result.GetAsyncHandle();
        const TString& message = GetCommonJsonHttp(asyncCode, params);
        while (result.IsAsync() && Now() < deadline) {
            Sleep(TDuration::Seconds(1));
            result = SendToProxy(message.data(), message.size());
        }
        return !result.IsAsync();
    }

    return false;
}

bool NSaas::TIndexingClient::WaitAsync(TSendResult& result, TDuration timeout) const {
    const TInstant deadline = timeout != TDuration::Zero() ? Now() + timeout : TInstant::Max();
    return WaitAsync(result, deadline);
}

TSendResult TIndexingClient::SendJson(const TAction& action, const TSendParams& params) const {
    return Send(action, JSON_TYPE, params);
}

TSendResult TIndexingClient::SendJsonRef(const TAction& action, const TSendParams& params) const {
    return Send(action, JSON_REF_TYPE, params);
}

TSendResult TIndexingClient::SendProto(const TAction& action, const TSendParams& params) const {
    return Send(action, PROTO_TYPE, params);
}

TString TIndexingClient::GetIndexingUrl(const TSendParams& params) const {
    TString noCgiUrl(Url);
    TCgiParameters cgi;
    if (Url.find('?') != TString::npos) {
        TVector<TString> splitted(SplitString(Url, "?"));
        if (splitted.size() != 2)
            ythrow yexception() << "incorrect url";
        noCgiUrl = splitted[0];
        cgi.Scan(splitted[1]);
    }

    if (params.VerboseReply)
        cgi.InsertUnescaped("debug", "yes");
    if (!params.Realtime)
        cgi.InsertUnescaped("realtime", "no");
    if (!!params.Auth)
        cgi.InsertUnescaped("auth", params.Auth);
    if (params.Trace)
        cgi.InsertUnescaped("trace", "yes");
    if (params.InstantReply)
        cgi.InsertUnescaped("instant_reply", "yes");
    cgi.InsertUnescaped("timeout", ToString(Timeout.MilliSeconds()));
    return noCgiUrl + '?' + cgi.Print();
}

TString NSaas::TIndexingClient::GetCommonJsonHttp(const TString& body, const TSendParams& params) const {
    return
        "POST " + GetIndexingUrl(params) + " HTTP/1.1\r\n"
        "Host: " + Host + "\r\n"
        "Content-Type: application/json\r\n"
        "Content-Length: " + ToString(body.size()) + "\r\n"
        "Connection: Keep-Alive\r\n"
        "\r\n" + body;
}

TString TIndexingClient::GetCommonJsonHttp(const NSaas::TAction& action, const TSendParams& params) const {
    const TString& body = action.BuildJsonMessage();
    return GetCommonJsonHttp(body, params);
}

namespace {
    THttpHeaders JsonRefHeaders = []() {
        THttpHeaders headers;
        headers.AddHeader("Connection", "Keep-Alive");
        return headers;
    }();
}

TString TIndexingClient::GetJsonRefHttp(const NSaas::TAction& action, const TSendParams& params) const {
    TToJsonContext context(TToJsonContext::JSON_REF);
    TString jsonMessage = NUtil::JsonToString(action.ToJson(context), true);
    context.Parts.push_back(std::make_pair(TString("json_message"), jsonMessage));
    return BuildMultipartHttpRequest(context.Parts, GetIndexingUrl(params), Host, DefaultBoundary, JsonRefHeaders);
}

TString TIndexingClient::GetJsonProtoHttp(const NSaas::TAction& action, const TSendParams& params) const {
    const auto& proto = action.ToProtobuf();
    TString buffer;
    buffer.ReserveAndResize(proto.ByteSize());
    Y_PROTOBUF_SUPPRESS_NODISCARD proto.SerializeToArray(buffer.begin(), buffer.length());
    return GetCommonJsonHttp(buffer, params);
}

TString TIndexingClient::GetHttpMessage(const NSaas::TAction& action, const TString& type, const TSendParams& params) const {
    if (JSON_TYPE == type) {
        return GetCommonJsonHttp(action, params);
    } else if (JSON_REF_TYPE == type) {
        return GetJsonRefHttp(action, params);
    } else if (PROTO_TYPE == type) {
        return GetJsonProtoHttp(action, params);
    } else {
        throw yexception() << "unknown sender type: " << type;
    }
}

NSaas::TSendResult NSaas::TIndexingClient::SendToProxy(const char* data, size_t length) const {
    const NDns::TResolveInfo ri(Host, Port);
    const NDns::TResolvedHost* rh = NDns::CachedResolve(ri);
    if (!rh) {
        return TSendResult::FromCurrentExeption(TSendResult::ESendResult::srNetworkResolutionError);
    }

    TSocket socket(rh->Addr, Timeout);
    TSocketInput si(socket);
    TSocketOutput so(socket);
    THttpOutput output(&so);

    socket.SetKeepAlive(true);
    socket.SetZeroLinger();

    TInstant queryStart = Now();
    output.EnableKeepAlive(true);
    output.Write(data, length);

    DEBUG_LOG << "query_time: " << Now() - queryStart << Endl;

    TInstant replyStart = Now();
    THttpInput input(&si);
    TString reply = input.ReadAll();
    DEBUG_LOG << "reply_head: " << input.FirstLine() << "; reply_body: " << reply << "; reply_time: " << Now() - replyStart << Endl;

    return TSendResult::FromProxyReply(input.FirstLine(), reply);
}

void NSaas::InitializeLogging() {
    DoInitGlobalLog("cerr", TLOG_INFO, false, false);
}
