#include "admin_manager.h"
#include "master_main.h"
#include "master_process.h"

#include <balancer/kernel/balancing/updater.h>
#include <balancer/kernel/http/parser/httpdecoder.h>
#include <balancer/kernel/http/parser/output.h>
#include <balancer/kernel/http/parser/response_builder.h>

#include <library/cpp/cgiparam/cgiparam.h>

namespace {
    template <class T>
    bool ParseCgiParam(const TCgiParameters& cgiParams, NSrvKernel::TChunksOutputStream& out,
                       const TStringBuf& name, T& res, bool required = false) noexcept
    {
        auto iter = cgiParams.Find(name);
        if (iter != cgiParams.end()) {
            if (!TryFromString<T>(iter->second, res)) {
                out << "Unable to parse: '" << iter->second << "'.\n";
                return false;
            } else {
                return true;
            }
        } else {
            if (required) {
                out << "Required parameter '" << name
                    << "' is missing.\n";
            }
            return !required;
        }
    }
}

namespace NSrvKernel::NProcessCore {
    TAdminManager::TAdminManager(TMasterProcess& masterProcess)
        : MasterProcess_(masterProcess)
        , EventsRegexp_("/admin/events(/(list|(echo|call|json)/([a-z_][0-9a-z_]*))|/.*|)")
    {}

    TError TAdminManager::MakeAdminAnswer(IIoInput* input, IIoOutput* output) noexcept {
        TFromClientDecoder decoder(input);
        TRequest request;
        Y_PROPAGATE_ERROR(decoder.ReadRequest(request, TInstant::Max()));

        THttpOutput httpOut{output, 1, false, false, false};

        TChunksOutputStream out;

        TStringBuf command;
        TStringBuf event;
        int errorCode = 200;
        auto url = request.RequestLine().GetURL();
        if (ExtractEvent(url, command, event)) {
            TEventData eventData{ event, out };

            if (command == "list") {
                MasterProcess_.ListEventHandlers(eventData);
            } else if (command == "json") {
                MasterProcess_.ProcessJsonEvent(eventData);
            } else if (command == "call") {
                if (event == "dbg_master_pid") {
                    MasterProcess_.PrintMasterPid(eventData);
                } else if (event == "reset_dns_cache"){
                    MasterProcess_.ResetDnsCache(eventData);
                } else {
                    MasterProcess_.CallEvent(eventData, false);
                }
            } else {
                out << "Usage:\n"
                       "    /admin/events/list - list all event handlers\n"
                       "    /admin/events/call/<Event> - execute corresponding in child processes\n";
            }
        } else if (url != "/") {
            const TCgiParameters cgiParams{ request.RequestLine().CGI.AsStringBuf() };

            TString action;
            bool triggered = false;

            if (ParseCgiParam(cgiParams, out, "?action", action)) {

                triggered = true;
                TStringBuf e;
                TEventData eventData(e, out);

                if (action == "shutdown") {
                    MasterProcess_.ShutDown(eventData);
                } else if (action == "graceful_shutdown") {
                    if (!TriggerGracefulShutdown(eventData, request.RequestLine(), out)) {
                        triggered = false;
                    }
                } else if (action == "reload_config") {
                    if (!TriggerReloadConfig(eventData, request.RequestLine(), out)) {
                        triggered = false;
                    }
                } else if (action == "sandboxtaskid") {
                    eventData.SetEvent("sandbox_task_id");
                    MasterProcess_.SandboxTaskId(eventData);
                }  else if (action == "reopenlog") {
                    eventData.SetEvent(action);
                    MasterProcess_.ReopenLog(eventData);
                } else if (action == "version") {
                    eventData.SetEvent(action);
                    MasterProcess_.Version(eventData);
                } else if (action == "show_listen") {
                    eventData.SetEvent(action);
                    MasterProcess_.ShowListen(eventData);
                } else if (action == "show_tag") {
                    eventData.SetEvent(action);
                    MasterProcess_.GetConfigTag(eventData);
                } else if (action == "reset_dns_cache") {
                    eventData.SetEvent(action);
                    MasterProcess_.ResetDnsCache(eventData);
                } else if (action == "dbg_workers_ready") {
                    eventData.SetEvent(action);
                    MasterProcess_.DbgWorkersReady(eventData);
                } else {
                    triggered = false;
                }
                errorCode = eventData.IsError() ? 500 : 200;
            }

            if (!triggered) {
                errorCode = 400;
                out << "\nIncorrect command\n"
                       "Usage: admin?action=<Command>\n"
                       "Command:\n"
                       "    reopenlog - reopens log\n"
                       "    shutdown - shut down balancer\n"
                       "    version - return balancer version, build dir, time, compile time options\n"
                       "    show_listen - return xml with all balancer listen addrs\n"
                       "    show_tag - show config tag\n"
                       "    reset_dns_cache - reset dns cache\n"
                       "    sandboxtaskid - show sandbox task id\n"
                       "    graceful_shutdown[&timeout=..][&close_timeout=..]  - stop listening for new connections but continue processing the old ones\n"
                       "    reload_config&new_config_path=newconf.lua[&timeout=..][&start_delay=..][&V_variable=value] - reload config to new_config_path\n"
                       "\n"
                       "Also, take look at admin/events API:\n"
                       "    /admin/events/list - list all event handlers\n"
                       "    /admin/events/echo/<Event> - list corresponding handlers without execution\n"
                       "    /admin/events/call/<Event> - execute corresponding in child processes'\n";
            }
        }

        TChunkList outData = std::move(out.Chunks());
        TResponse response;

        Y_TRY(TError, error) {
            Y_PROPAGATE_ERROR(httpOut.SendHead(BuildResponse().Version11().Code(errorCode), false, TInstant::Max()));
            if (!outData.Empty()) {
                Y_PROPAGATE_ERROR(httpOut.Send(std::move(outData), TInstant::Max()));
            }

            Y_PROPAGATE_ERROR(httpOut.SendEof(TInstant::Max()));
            return SkipAll(input, TInstant::Max());
        } Y_CATCH {
            MasterProcess_.Log() << "admin error: " << GetErrorMessage(error) << Endl;
            return error;
        }
        return {};
    }

    bool TAdminManager::TriggerGracefulShutdown(TEventData& event, const TRequestLine& request,
        TChunksOutputStream& out) const noexcept {
        const TStringBuf cgi = request.CGI.AsStringBuf();
        TCgiParameters cgiParams{ cgi };
        TGracefulShutdownOpts opts;

        if (
            ParseCgiParam(cgiParams, out, "cooldown", opts.CoolDown) &&
            ParseCgiParam(cgiParams, out, "timeout", opts.Timeout) &&
            ParseCgiParam(cgiParams, out, "close_timeout", opts.CloseTimeout)
        ) {
            MasterProcess_.GracefulShutdown(event, opts);
            return true;
        }
        return false;
    }

    bool TAdminManager::TriggerReloadConfig(TEventData& event, const TRequestLine& request,
        TChunksOutputStream& out) const noexcept {
        const TStringBuf cgi = request.CGI.AsStringBuf();
        TCgiParameters cgiParams{ cgi };
        TReloadConfigOpts opts;
        opts.GracefulShutdownOpts.Timeout = TDuration::Seconds(60);

        if (ParseCgiParam(cgiParams, out, "new_config_path", opts.NewConfigPath, true) &&
            ParseCgiParam(cgiParams, out, "timeout", opts.Timeout) &&
            ParseCgiParam(cgiParams, out, "C", opts.NewCoroStackSize) &&
            ParseCgiParam(cgiParams, out, "start_delay", opts.WorkerStartDelay) &&
            ParseCgiParam(cgiParams, out, "save_globals", opts.SaveGlobals) &&
            ParseCgiParam(cgiParams, out, "graceful_shutdown_cooldown", opts.GracefulShutdownOpts.CoolDown) &&
            ParseCgiParam(cgiParams, out, "graceful_shutdown_timeout", opts.GracefulShutdownOpts.Timeout) &&
            ParseCgiParam(cgiParams, out, "graceful_shutdown_close_timeout", opts.GracefulShutdownOpts.CloseTimeout) &&
            ParseCgiParam(cgiParams, out, "skip_block_requests", opts.GracefulShutdownOpts.SkipBlockRequests))
        {
            TStringBuf configParamPrefix = "V_";

            for (const auto& p : cgiParams) {
                const TString& key = p.first;
                const TString& value = p.second;

                if (!key.StartsWith(configParamPrefix)) {
                    continue;
                }

                TString param = key.substr(configParamPrefix.size());
                opts.NewGlobals[param] = value;
            }

            MasterProcess_.ReloadConfig(event, opts);
            return true;
        }
        return false;
    }


    bool TAdminManager::ExtractEvent(TStringBuf uri, TStringBuf& command, TStringBuf& event) const noexcept {
        TVector<TStringBuf> buffers;

        // 5 because: /admin/events/call/event_name; /call/event_name; call/event_name; call; event_name;
        if (EventsRegexp_.Extract(uri, &buffers, false) && buffers.size() == 5) {
            event = buffers.back();
            buffers.pop_back();

            if (event.empty()) {
                event = buffers.back();
                buffers.pop_back();
            }

            command = buffers.back();

            return true;
        } else {
            return false;
        }
    }
}
