#include <library/cpp/http/server/http.h>
#include <library/cpp/http/client/client.h>
#include <library/cpp/json/json_reader.h>
#include <library/cpp/logger/global/global.h>
#include <library/cpp/tvmauth/client/facade.h>
#include <library/cpp/tvmauth/client/logger.h>
#include <library/cpp/http/misc/httpcodes.h>

#include <util/stream/output.h>
#include <util/string/strip.h>
#include <util/system/env.h>
#include <util/string/builder.h>
#include <util/generic/yexception.h>
#include <util/generic/hash_set.h>

struct TFetchParams {
    TString Url;
    TString Host;
    TString ServiceTicket{};
    TString UserTicket{};
};

auto Fetch(const TFetchParams& params) {
    NHttp::TFetchOptions options;

    // TODO
    //options.RetryCount = opts.RetryCount;
    //options.RetryDelay = opts.RetryDelay;
    //options.Timeout = opts.Timeout;

    //options.ContentType = "application/json";

    TVector<TString> headers;
    if (params.Host) {
        options.SetCustomHost(params.Host);
    }
    if (params.ServiceTicket) {
        headers.push_back("X-Ya-Service-Ticket: " + params.ServiceTicket);
    }
    if (params.UserTicket) {
        headers.push_back("X-Ya-User-Ticket: " + params.UserTicket);
    }

    DEBUG_LOG << "curl -s ";
    for (const auto& hdr : headers) {
        DEBUG_LOG << " -H \"" << hdr << "\" \\\n";
    }
    DEBUG_LOG << " \"" << params.Url << "\"\n";

    NHttp::TFetchQuery query(params.Url, headers, options);

    return NHttp::Fetch(query);
}

TString GetSessionId(const TString& cookies) {
    TStringBuf pairs(cookies);
    while (pairs) {
        auto kv = pairs.NextTok(';');
        auto name = StripString(kv.NextTok('='));
        if (name == "Session_id") {
            return TString{StripString(kv)};
        }
    }
    return {};
}

static const ui32 StableSpTvmId = 2011468;
static const ui32 PrestableSpTvmId = 2010902;

struct TConfig {
    TString TvmSecret = GetEnv("TVM_SECRET");
    ui32 TvmSelfClientId = 2020150;
    ui32 BlackBoxTvmId = 223;
    TString BlackBoxUrl = "https://blackbox.yandex-team.ru/blackbox";
    //TString BlackBoxUrl = "https://localhost:17443/blackbox";
    THashMap<TString, ui32> SearchProxiesTvmIds{
        // debug
        //{"localhost", PrestableSpTvmId},

        // old
        {"saas-searchproxy.yandex.net", StableSpTvmId},
        {"saas-searchproxy-kv.yandex.net", StableSpTvmId},
        {"saas-searchproxy-middle-kv.yandex.net", StableSpTvmId},

        // new
        {"stable-market.saas.yandex.net", StableSpTvmId},
        {"stable-hamster.saas.yandex.net", StableSpTvmId},
        {"stable-middle-kv.saas.yandex.net", StableSpTvmId},
        {"stable-kv.saas.yandex.net", StableSpTvmId},
        {"stable-knn.saas.yandex.net", StableSpTvmId},
        {"stable.saas.yandex.net", StableSpTvmId},

        // yp
        {"saas-yt-yp-proxy.yandex.net", StableSpTvmId},

        // prestable
        {"saas-searchproxy-prestable.yandex.net", PrestableSpTvmId},

        //saas-searchproxy-testing.yandex.net
    };
};

using TMakeTvmTicket = std::function<TString(ui32)>;

class TRequest: public TRequestReplier {
public:
    TRequest(const TConfig& config, TMakeTvmTicket&& makeTvmTicket)
        : Config(config)
        , MakeTvmTicket(std::move(makeTvmTicket))
    {
    }

    bool DoReply(const TReplyParams& params) override {
        try {
            DoDoReply(params);
        } catch (...) {
            params.Output << "HTTP/1.0 500 Internal Server Error\r\n\r\n" << CurrentExceptionMessage() << '\n';
        }
        return true;
    }

    void DoDoReply(const TReplyParams& params) {
        DEBUG_LOG << params.Input.FirstLine() << Endl;

        TStringBuf firstLine = params.Input.FirstLine();
        firstLine.NextTok(' ');
        auto targetUrl = firstLine.NextTok(' ');
        targetUrl.SkipPrefix("/");
        DEBUG_LOG << "targetUrl: [" << targetUrl << "]" << Endl;

        auto spProxyHost = targetUrl;
        spProxyHost.SkipPrefix("http://") || spProxyHost.SkipPrefix("https://");
        spProxyHost = spProxyHost.NextTok('/');
        spProxyHost = spProxyHost.NextTok(':');
        DEBUG_LOG << "spProxyHost: [" << spProxyHost << "]" << Endl;

        ui32 spProxyTvmId = 0;
        if (auto* id = Config.SearchProxiesTvmIds.FindPtr(spProxyHost)) {
            spProxyTvmId = *id;
        } else {
            params.Output << "HTTP/1.0 404 Not Found\r\n\r\n" << "Not supported searchproxy balancer host\n";
            return;
        }

        TString sessionId, userIp, httpHost;

        for (const auto& hdr : params.Input.Headers()) {
            DEBUG_LOG << hdr.Name() << ": " << hdr.Value() << Endl;
            if (hdr.Name() == "Cookie") { // TODO lower case
                sessionId = GetSessionId(hdr.Value());
            } else if (hdr.Name() == "X-Forwarded-For-Y") {
                userIp = hdr.Value();
            } else if (hdr.Name() == "Host") {
                httpHost = hdr.Value();
            }
        }
        DEBUG_LOG << '\n';

        if (!sessionId) {
            params.Output << "HTTP/1.0 400 Bad Request\r\n\r\n" << "No sessionId cookie\n";
            return;
        }

        const TString blackboxUrl = TStringBuilder() << Config.BlackBoxUrl
            << "?method=sessionid"
            << "&format=json"
            << "&get_user_ticket=yes"
            << "&userip=" << userIp
            << "&host=" << httpHost
            << "&sessionid=" << sessionId // TODO Encode
        ;

        const auto bbResp = Fetch({
            .Url = blackboxUrl,
            .ServiceTicket = Config.BlackBoxTvmId ? MakeTvmTicket(Config.BlackBoxTvmId) : TString{},
            //.Host = "blackbox.yandex-team.ru",
        });
        DEBUG_LOG << "BlackBox: " << bbResp->Code << '\t' << bbResp->Data << '\n';
        Y_ENSURE(bbResp->Code == 200, "BlackBox answer is not 200 OK");

        TString userTicket;
        {
            NJson::TJsonValue json;
            Y_ENSURE(NJson::ReadJsonTree(bbResp->Data, &json), "Failed to parse BlackBox json response");

            if (json.GetMap().at("status").GetMap().at("value").GetString() != "VALID") {
                params.Output << "HTTP/1.0 403 OK\r\n\r\n" << "bb not VALID" << '\n';
                return;
            }

            userTicket =json.GetMap().at("user_ticket").GetString();
            Y_ENSURE(userTicket, "No user_ticket in BlackBox response");
        }

        const auto resp = Fetch({
            .Url = TString{targetUrl},
            .ServiceTicket = MakeTvmTicket(spProxyTvmId),
            .UserTicket = userTicket
        });

        auto& out = params.Output;
        out << "HTTP/1.0 200 OK\r\n";
        out << "Content-Type: text/plain; charset=utf-8\r\n\r\n";

        out << "--------------------------------------------------\n";
        out << resp->HttpVersion << ' ' << resp->Code << ' ' << HttpCodeStr(resp->Code) << '\n';
        for (const auto& hdr : resp->Headers) {
            hdr.OutTo(&out);
        }
        out << '\n';
        out << "--------------------------------------------------\n";
        out << resp->Data;
    }

private:
    const TConfig& Config;
    TMakeTvmTicket MakeTvmTicket;
};

class TCallBack: public THttpServer::ICallBack {
public:
    NTvmAuth::NTvmApi::TClientSettings::TDstVector CollectTvmDstIds() const {
        THashSet<ui32> ids;
        for (const auto& [_, id] : Config.SearchProxiesTvmIds) {
            ids.insert(id);
        }
        if (const auto id = Config.BlackBoxTvmId) {
            ids.insert(id);
        }
        return {ids.begin(), ids.end()};
    }

    TCallBack() {
        Y_ENSURE(Config.TvmSecret);
        NTvmAuth::NTvmApi::TClientSettings settings;
        settings.SetSelfTvmId(Config.TvmSelfClientId);
        settings.EnableServiceTicketsFetchOptions(Config.TvmSecret, CollectTvmDstIds());
        TvmClient.ConstructInPlace(settings, new NTvmAuth::TCerrLogger(7));
    }

    TClientRequest* CreateClient() override {
        return new TRequest(
            Config,
            [this](ui32 id) -> TString {
                return TvmClient->GetServiceTicketFor(id);
            }
        );
    }

private:
    TConfig Config;
    TMaybe<NTvmAuth::TTvmClient> TvmClient;
};

int main() {
    //InitGlobalLog2Console(TLOG_DEBUG);
    TCallBack cb;
    THttpServer server(&cb);
    Y_ENSURE(server.Start());
    server.Wait();
    return 0;
}
