#include "module.h"

#include <balancer/kernel/coro/cleanable_coro_storage.h>
#include <balancer/kernel/http/parser/httpencoder.h>
#include <balancer/kernel/module/module.h>
#include <balancer/kernel/requester/requester.h>

#include <yweb/weblibs/signurl/signurl.h>
#include <yweb/weblibs/signurl/clickhandle.h>

#include <kernel/signurl/keyholder.h>

#include <util/generic/maybe.h>
#include <util/generic/scope.h>
#include <util/generic/string.h>
#include <util/generic/vector.h>
#include <util/stream/str.h>
#include <util/stream/null.h>

#include <utility>

using namespace NConfig;
using namespace NSrvKernel;

class TClickHandleOptions : public ClickHandle::TOptions, public TWithDefaultInstance<TClickHandleOptions> {
public:
    TClickHandleOptions() noexcept
        : ClickHandle::TOptions()
    {
        EnableProxy = false;
        ReportToCerr = false;
    }
};

Y_TLS(click) {
    bool KillSwitchFileExists() const noexcept {
        return KillSwitchChecker.Exists();
    }

    TSharedFileExistsChecker KillSwitchChecker;
    TCleanableCoroStorage Runners;
};

MODULE_WITH_TLS_BASE(click, TModuleWithSubModule) {
public:
    TModule(const TModuleParams& mp)
        : TModuleBase(mp) {
        Config->ForEach(this);

        if (!Submodule_) {
            ythrow TConfigParseError() << "no submodule configured for click";
        }

    }

private:
    struct TLogger : public ClickLogger, public TWithMutableInstance<TLogger> {
        TLogger()
            : ClickLogger("", nullptr, nullptr) {
        }
    };

private:
    START_PARSE {
        if (key == "keys") {
            if (!SignKeys_) {
                SignKeys_.Reset(new NSignUrl::TSignKeys());
            }

            SignKeys_->LoadKeysFromFile(value->AsString());
            return;
        }

        if (key == "json_keys") {
            if (!SignKeys_) {
                SignKeys_.Reset(new NSignUrl::TSignKeys());
            }

            SignKeys_->LoadKeysFromJsonFile(value->AsString());
            return;
        }

        ON_KEY("file_switch", KillSwitchFile_) {
            return;
        }

        {
            Submodule_.Reset(Loader->MustLoad(key, Copy(value->AsSubConfig())).Release());
            return;
        }
    }
    END_PARSE

    THolder<TTls> DoInitTls(IWorkerCtl* process) override {
        auto tls = MakeHolder<TTls>();
        if (!!KillSwitchFile_) {
            tls->KillSwitchChecker = process->SharedFiles()->FileChecker(KillSwitchFile_, TDuration::Seconds(1));
        }
        return tls;
    }

    static bool MayHaveBody(const TConnDescr& descr) noexcept {
        if (Y_LIKELY(descr.Request)) {
            const auto& props = descr.Request->Props();
            return props.ChunkedTransfer || props.ContentLength > 0;
        }

        return false;
    }

    TMaybe<NSignUrl::TUnsignedUrl> TryUnsignUrl(const TString& url) const noexcept {
        try {
            return MakeMaybe(NSignUrl::UnsignUrl(url, SignKeys_.Get()));
        } catch (yexception&) {
        }
        return TMaybe<NSignUrl::TUnsignedUrl>();
    }

    void AsyncClickStart(const TConnDescr& descr, TTls& tls) const noexcept {
        tls.Runners.Emplace("async_click_runner", &descr.Process().Executor(),
            [this, &process = descr.Process()](TRequest&& request, TRequestHash hash) {
                TAsyncRequester requester(*Submodule_, nullptr, process, hash);
                Y_UNUSED(requester.Requester().Request(std::move(request)));
            }, *descr.Request, descr.Hash
        );
    }

    TError DoRun(const TConnDescr& descr, TTls& tls) const noexcept override {
        tls.Runners.EraseFinished();

        if (MayHaveBody(descr)) {
            descr.ExtraAccessLog << " not_applicable";
            Y_PROPAGATE_ERROR(Submodule_->Run(descr));
        } else if (tls.KillSwitchFileExists()) {
            descr.ExtraAccessLog << " click_lib_disabled";
            Y_PROPAGATE_ERROR(Submodule_->Run(descr));
        } else if (!SignKeys_) {
            descr.ExtraAccessLog << " no_click_keys";
            Y_PROPAGATE_ERROR(Submodule_->Run(descr));
        } else {
            TChunksOutputStream output;

            const auto url = descr.Request->RequestLine().GetURL();

            TVector<std::pair<TString, TString>> headers;
            headers.reserve(descr.Request->Headers().Size());

            for (const auto& i : descr.Request->Headers()) {
                for (const auto& hdrVal : i.second) {
                    headers.emplace_back(TString(i.first.AsStringBuf()), TString(hdrVal.AsStringBuf()));
                }
            }

            auto unsignedUrl = TryUnsignUrl(url);

            ClickHandle handle{ output, SignKeys_.Get(), "", headers, TLogger::Instance(),
                                TClickHandleOptions::Instance() };

            // TODO(velavokr): limit POSTs
            if ((!unsignedUrl || !unsignedUrl->Proxy) &&
                handle.Process(url.data(), unsignedUrl.Get())) // won't throw
            {
                Y_ASSERT(descr.Request);
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "click_lib_answer");

                TChunksInput chunksIo{ std::move(output.Chunks()) };
                TFromBackendDecoder decoder{&chunksIo, *descr.Request};
                TResponse response;
                Y_PROPAGATE_ERROR(decoder.ReadResponse(response, TInstant::Max()));
                descr.ExtraAccessLog << " click_lib_answer " << response.ResponseLine().StatusCode;
                Y_PROPAGATE_ERROR(descr.Output->SendHead(std::move(response), false, TInstant::Max()));
                Y_PROPAGATE_ERROR(Transfer(&decoder, descr.Output, TInstant::Max()));
                AsyncClickStart(descr, tls);
                return {};
            } else {
                if (unsignedUrl && unsignedUrl->Proxy) {
                    descr.ExtraAccessLog << " click_lib_proxy";
                } else {
                    descr.ExtraAccessLog << " click_lib_answer 0";
                }
                Y_PROPAGATE_ERROR(Submodule_->Run(descr));
            }
        }
        return {};
    }

    bool DoExtraAccessLog() const noexcept override {
        return true;
    }

private:
    THolder<NSignUrl::TSignKeys> SignKeys_;
    TString KillSwitchFile_;
};

IModuleHandle* NModClick::Handle() {
    return TModule::Handle();
}
