#include "controller.h"

#include <saas/library/daemon_base/actions_engine/controller_script.h>
#include <saas/library/daemon_base/threads/messages.h>
#include <saas/library/daemon_base/common/common.h>
#include <saas/util/bomb.h>
#include <saas/util/json/json.h>
#include <saas/util/logging/messages.h>
#include <saas/util/network/http_request.h>

#include <search/common/profile.h>

#include <library/cpp/balloc/optional/operators.h>
#include <library/cpp/string_utils/base64/base64.h>

#include <util/string/join.h>
#include <util/stream/zlib.h>

#define DEFINE_COMMON_CONTROLLER_COMMAND(name, description) \
class T ## name ## Command : public TController::TCommandProcessor {\
    virtual TString GetInfoMessage() const { return description ; }\
protected:\
    virtual TString GetName() const { return #name ; }\
    virtual void DoProcess();\
};\
    TController::TCommandFactory::TRegistrator<T ## name ## Command> Registrator ## name(#name);\
    void T ## name ## Command::DoProcess() {


DEFINE_COMMON_CONTROLLER_COMMAND(help, "arguments: item (not required)")
    if (GetCgi().Find("item") == GetCgi().end()) {
        TSet<TString> common;
        TController::TCommandFactory::GetRegisteredKeys(common);
        TSet<TString> serverRelated = Owner->GetCommands();
        TString commonCommands = JoinRange(", ", common.begin(), common.end());
        TString serverCommands = JoinRange(", ", serverRelated.begin(), serverRelated.end());

        Write("common commands", commonCommands);
        Write("this server commands", serverCommands);
    } else {
        const TString requiredCommand = GetCgi().Get("item");
        const TString description = Owner->GetCommandInfo(requiredCommand);
        Write("info", requiredCommand + ": " + description);
    }
END_CONTROLLER_COMMAND


DEFINE_COMMON_CONTROLLER_COMMAND(restart, "arguments: reread_config=boolean, sleep=time, stop_timeout=time")
    const TCgiParameters& params = GetCgi();
    ui32 rigidStopLevel = ParseRigidStopLevel(params);
    bool reread = (params.Find("reread_config") != params.end()) && FromString<bool>(params.Get("reread_config"));
    const TString sleepStr = params.Get("sleep");
    TDuration sleepTimeout;
    if (!!sleepStr)
        sleepTimeout = TDuration::Parse(sleepStr);
    const TString stopTimeoutStr = params.Get("stop_timeout");
    TDuration stopTimeout;
    if (!!stopTimeoutStr)
        stopTimeout = TDuration::Parse(stopTimeoutStr);
    TController::TRestartServerStatistics statistics;
    TBomb bomb(stopTimeout, "stop on restart too long");
    Owner->DestroyServer(rigidStopLevel, &params, &statistics, sleepTimeout);
    Owner->ClearServerDataStatus();
    bomb.Deactivate();
    SetStatus(stsStarted, "STOPPED");
    Owner->RestartServer(rigidStopLevel, &params, reread, &statistics, sleepTimeout);
    if (Owner->GetStatus() != NController::ssbcActive) {
        ythrow yexception() << Owner->GetStatus();
    }
    Write("stop_time", statistics.StopTimeMilliseconds);
    Write("start_time", statistics.StartTimeMilliseconds);
    Write("rigid_level", rigidStopLevel);
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(reset, "resetting controller")
    Owner->Reset(0);
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(reopenlog, "reopen logfile")
    auto start = TInstant::Now();
    TLogBackend::ReopenAllBackends(false);
    Owner->GetDaemonConfig()->ReopenLog();
    SendGlobalMessage<TMessageReopenLogs>();
    INFO_LOG << "Logs successfully reopened, time spent: " << (TInstant::Now() - start).MicroSeconds() << "us" << Endl;
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(get_status, "")
    Write("status", ToString(Owner->GetStatus()));
    const TServerInfo info = Owner->GetServerInfo(/* controllerOnly = */ true, /* isHumanReadable = */ false);
    Write("result", info);
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(no_file_operations, "arguments: set=boolean")
    TString value = GetCgi().Get("set");
    if (!!value)
        Owner->SetNoFileOperations(FromString<bool>(value));
    Write("result", Owner->GetNoFileOperations());
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(get_async_command_info, "arguments: id=string")
    TString taskId = GetCgi().Get("id");
    Owner->GetAsyncCommondInfo(taskId, *this);
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(stop, "arguments: sleep=time")
    const TCgiParameters& params = GetCgi();
    ui32 rigidStopLevel = ParseRigidStopLevel(params);
    const TString sleepStr = params.Get("sleep");
    TDuration sleepTimeout;
    if (!!sleepStr)
        sleepTimeout = TDuration::Parse(sleepStr);
    Owner->DestroyServer(rigidStopLevel, &params, nullptr, sleepTimeout);
    Write("rigid_level", rigidStopLevel);
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(abort, "arguments: coredump=boolean")
    if (IsTrue(GetCgi().Get("coredump"))) {
        FAIL_LOG("Aborted by user command");
    } else {
        NOTICE_LOG << "Soft abort invoked by user command" << Endl;
        _exit(EXIT_FAILURE);
    }
END_CONTROLLER_COMMAND

namespace {
    class TStopThread : public IThreadFactory::IThreadAble {
        TController* Owner;
    public:
        TStopThread(TController* owner)
            : Owner(owner)
                                            {}
        void DoExecute() override {
            ThreadDisableBalloc();
            TThread::SetCurrentThreadName("CtrlStopThread");
            THolder<TStopThread> suicide(this);
            Owner->Stop(0);
        }
    };
};

DEFINE_COMMON_CONTROLLER_COMMAND(shutdown, "arguments: sleep=time")
    const TCgiParameters& params = GetCgi();
    ui32 rigidStopLevel = ParseRigidStopLevel(params);
    const TString sleepStr = params.Get("sleep");
    TDuration sleepTimeout;
    if (!!sleepStr)
        sleepTimeout = TDuration::Parse(sleepStr);
    Owner->DestroyServer(rigidStopLevel, &params, nullptr, sleepTimeout);
    SystemThreadFactory()->Run(new TStopThread(Owner));
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(put_file, "arguments: filename=string")
    const TString filename = GetCgi().Get("filename");
    if (!filename)
        throw yexception() << "there is no filename";
    TStringBuf data = GetPostBuffer();
    Owner->WriteFile(filename, data);
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(take_file, "arguments: filename=string, from_host=string, from_port=ui16, sleep_duration_ms=ui64, url=string")
    const TString& filename = GetCgi().Get("filename");
    if (!filename)
        throw yexception() << "there is no filename";
    const TString& fromHost = GetCgi().Get("from_host");
    if (!fromHost)
        throw yexception() << "there is no from_host";
    const TString& fromPortStr = GetCgi().Get("from_port");
    if (!fromPortStr)
        throw yexception() << "there is no from_port";
    ui16 fromPort = FromString<ui16>(fromPortStr);
    ui64 sleepDurationMs = 100;
    if (GetCgi().Has("sleep_duration_ms"))
        sleepDurationMs = FromString<ui64>(GetCgi().Get("sleep_duration_ms"));
    const TString& url = Base64Decode(GetCgi().Get("url"));
    NUtil::THttpRequest request(url, TString(GetPostBuffer()));
    request.SetSleepingPause(sleepDurationMs);
    request.SetAttemptionsMax(5);
    NUtil::THttpReply reply;
    reply = request.Execute(fromHost, fromPort);
    if (reply.IsSuccessReply()) {
        Owner->WriteFile(filename, reply.Content());
        return;
    }
    ythrow yexception() << "error while take file: " << reply.ErrorMessage() << "/" << reply.Content();
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(delete_file, "arguments: filename=string")
    const TString filename = GetCgi().Get("filename");
    if (!filename)
        throw yexception() << "there is no filename";
    Owner->GetFullPath(filename).ForceDelete();
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(get_file, "arguments: filename=string, size_limit=ui32")
    const TString filename = GetCgi().Get("filename");
    if (!filename)
        throw yexception() << "there is no filename";
    TString filePath = Owner->GetFullPath(filename);
    if (!TFsPath(filePath).Exists())
        throw yexception() << "file " << filePath << " does not exist";
    TFileStat stat(filePath);
    ui32 fileSizeLimit = GetCgi().Has("size_limit")
        ? FromString<ui32>(GetCgi().Get("size_limit")) : 1000000;
    if (stat.Size > fileSizeLimit)
        throw yexception() << "file is too big (" << stat.Size << "), will not read it";
    TString content = TFileInput(filePath).ReadAll();
    Write("result", content);
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(get_configs_hashes, "arguments: path=string, filter=string (regex are supported)")
    NJson::TJsonValue result(NJson::JSON_MAP);
    TString path(GetCgi().Get("path"));
    if (GetCgi().Has("filter")) {
        const TString& filterStr = GetCgi().Get("filter");
        if (filterStr) {
            TRegExMatch filter(filterStr.data());
            Owner->FillConfigsHashes(result, path, &filter);
        }
    } else
        Owner->FillConfigsHashes(result, path, nullptr);
    Write("result", result);
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(get_info_server, "arguments: filter=string (regex are supported), hr=bool")
    const auto& cgi = GetCgi();
    const bool isHumanReadable = cgi.Has("hr") && IsTrue(cgi.Get("hr", 0));
    const TServerInfo info = Owner->GetServerInfo(/* controllerOnly = */ false, isHumanReadable);
    if (GetCgi().Has("filter")) {
        NUtil::TJsonFilter filter;
        filter.SetIgnoreUnknownPath(true);
        filter.AddFilters(GetCgi(), "result.");
        NJson::TJsonValue filtered;
        filter.Apply(info, filtered);
        Write("result", filtered);
    } else
        Write("result", info);
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(get_queue_perf, "")
    TMessageGetQueuePerf message;
    SendGlobalMessage(message);
    Write("result", message.GetMessageIsProcessed() ? message.QueueByType : NJson::TJsonValue(false));
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(set_queue_opts, "'options' will completely replace the existing settings. arguments: name=string, options=string")
    //
    // "options" will completely replace the existing settings
    //
    // Note: not every change in options is possible here, or is tested enough
    // Please be careful.
    //
    const TString name = GetCgi().Get("name");
    if (!name)
        throw yexception() << "there is no name";
    const TString opts = GetCgi().Get("options");
    if (!opts)
        throw yexception() << "there are no options";

    TMessageSetQueueOpts message;
    message.QueueName = name;
    message.OptionsStr = opts;
    SendGlobalMessage(message);
    Write("result", message.GetMessageIsProcessed());
END_CONTROLLER_COMMAND

namespace {
    class TScriptLogger : public NDaemonController::IControllerAgentCallback {
    public:
        TScriptLogger(TController::TCommandProcessor& owner)
            : Result(NJson::JSON_ARRAY)
            , Owner(owner)
            , CurrentAction(nullptr)
        {}
        void OnAfterActionStep(const TActionContext& /*context*/, NDaemonController::TAction& action) override {
            CHECK_WITH_LOG(CurrentAction);
            *CurrentAction = action.SerializeExecutionInfo();
            Owner.Write("script", Result);
        }
        void OnBeforeActionStep(const TActionContext& /*context*/, NDaemonController::TAction& action) override {
            CurrentAction = &Result.AppendValue(NJson::JSON_MAP);
            *CurrentAction = action.SerializeExecutionInfo();
            Owner.Write("script", Result);
        }
    private:
        NJson::TJsonValue Result;
        TController::TCommandProcessor& Owner;
        NJson::TJsonValue* CurrentAction;
    };
}

DEFINE_COMMON_CONTROLLER_COMMAND(execute_script, "arguments: script_name=string")
    TString scriptName = GetCgi().Get("script_name");
    TString decoded = Base64Decode(GetPostBuffer());
    TStringInput si(decoded);
    TZLibDecompress decompressed(&si);
    NJson::TJsonValue scriptJson;
    if (!NJson::ReadJsonTree(&decompressed, &scriptJson))
        ythrow yexception() << "Errors in script " << scriptName << " json: " << decompressed.ReadAll();
    NJson::TJsonValue result;
    TScriptLogger logger(*this);
    NRTYScript::TScript script;
    if (!script.Deserialize(scriptJson))
        ythrow yexception() << "cannot deserialize script " << scriptName;
    result.InsertValue("name", scriptName);
    DEBUG_LOG << "Execute script " << NJson::WriteJson(&scriptJson, true, true) << Endl;
    if (Owner->GetServerDataStatus() == NController::FailedConfig)
        Owner->ClearServerDataStatus();
    script.Execute(16);
    scriptJson = script.Serialize();
    if (script.TasksCount(NRTYScript::TTaskContainer::StatusFailed)) {
        ERROR_LOG << "Execute script result: " << NJson::WriteJson(&scriptJson, true, true) << Endl;
        result.InsertValue("info", scriptJson);
        Write("result", result);
        ythrow yexception() << "script failed";
    } else {
        DEBUG_LOG << "Execute script result: " << NJson::WriteJson(&scriptJson, true, true) << Endl;
        result.InsertValue("info", script.GetStatusInfo());
        Write("result", result);
    }
    auto daemonConfig = Owner->GetDaemonConfig();
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(clear_data_status, "")
    Owner->ClearServerDataStatus(true);
    Write("result", "OK");
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(set_config, "set new config and restart server")
    bool isExists;
    bool canSetConfig = Owner->CanSetConfig(isExists);
    if(isExists && !canSetConfig)
        ythrow yexception() << "Incorrect set_config situation";
    NJson::TJsonValue result(NJson::JSON_MAP);
    if (!Owner->SetConfigFields(GetCgi(), result)) {
        Write("result", "");
        return;
    };
    Write("result", result);
    if (Owner->GetServerDataStatus() == NController::FailedConfig)
        Owner->ClearServerDataStatus();

    if (isExists) {
        Owner->RestartServer();
    }

    if (Owner->GetStatus() == NController::ssbcNotRunnable) {
        ythrow yexception() << Owner->GetStatus();
    }
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(get_config, "")
    NJson::TJsonValue result(NJson::JSON_MAP);
    if (!Owner->GetConfigFields(GetCgi(), result)) {
        Write("result", "");
        return;
            }
    Write("result", result);
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(get_metric, "arguments: update=boolean, name=string")
    if (IsTrue(GetCgi().Get("update"))) {
        GetGlobalMetrics().UpdateValues(true);
    }

    const TString& name = GetCgi().Get("name");
    auto result = GetMetricResult(name);
    if (!result.Defined()) {
        result = GetMetricResult(GetMetricsPrefix() + name);
    }
    if (!result.Defined()) {
        Write("error", "undefined metric");
        return;
    }

    NJson::TJsonValue rates;
    rates.InsertValue("average", result->AvgRate);
    rates.InsertValue("current", result->CurRate);
    rates.InsertValue("maximum", result->MaxRate);
    rates.InsertValue("minimum", result->MinRate);
    Write("value", result->Value);
    Write("rate", rates);
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(get_must_be_alive, "")
    Write("must_be_alive", Owner->GetMustBeAlive());
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(download_configs_from_dm, "arguments: dm_options=string (json config), slot_info=string (json config), config_version=string")
    if (!GetCgi().Has("dm_options"))
        ythrow yexception() << "there is no dm_options";
    if (!GetCgi().Has("slot_info"))
        ythrow yexception() << "there is no slot_info";
    TDaemonConfig::TControllerConfig::TDMOptions dm;
    dm.Deserialize(NUtil::JsonFromString(GetCgi().Get("dm_options")));
    const TString& version = GetCgi().Get("config_version");
    NSaas::TSlotInfo slotInfo;
    if(!slotInfo.DeserializeFromJson(NUtil::JsonFromString(GetCgi().Get("slot_info"))))
        ythrow yexception() << "invalid slot_info";
    Write("downloaded", Owner->DownloadConfigsFromDM(dm, version, &slotInfo));
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(enable_nprofile, "")
    NProfile::EnableGlobal();
    Write("enabled", NProfile::IsEnabledGlobal());
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(disable_nprofile, "")
    NProfile::DisableGlobal();
    Write("enabled", NProfile::IsEnabledGlobal());
END_CONTROLLER_COMMAND

DEFINE_COMMON_CONTROLLER_COMMAND(check_nprofile, "")
    Write("enabled", NProfile::IsEnabledGlobal());
END_CONTROLLER_COMMAND

#undef DEFINE_COMMON_CONTROLLER_COMMAND
