#include "controller.h"
#include "info.h"

#include <saas/library/daemon_base/actions_engine/get_conf/action.h>
#include <saas/library/daemon_base/metrics/messages.h>
#include <saas/library/daemon_base/metrics/metrics.h>
#include <saas/library/daemon_base/metrics/persistent.h>
#include <saas/library/daemon_base/protos/status.pb.h>
#include <saas/library/daemon_base/threads/rty_pool.h>

#include <saas/util/bomb.h>
#include <saas/util/logging/tskv_log.h>
#include <saas/util/system/copy_recursive.h>
#include <saas/util/system/dir_digest.h>

#include <google/protobuf/text_format.h>

#include <search/common/profile.h>

#include <library/cpp/balloc/optional/operators.h>
#include <library/cpp/digest/md5/md5.h>
#include <library/cpp/json/json_reader.h>
#include <library/cpp/json/json_writer.h>
#include <library/cpp/yconf/patcher/unstrict_config.h>
#include <library/cpp/yt/mlock/mlock.h>

#include <util/system/mem_info.h>
#include <util/system/mlock.h>
#include <util/system/rusage.h>

namespace {
    bool ParseFieldParam(const TCgiParameters& cgi, NJson::TJsonValue& config, TString& prefix) {
        prefix = cgi.Has("prefix") ? cgi.Get("prefix") : TString("server");
        prefix.append('.');
        TStringStream fields;
        fields << cgi.Get("fields");
        if (fields.empty())
            return false;

        if (!NJson::ReadJsonTree(&fields, &config))
            ythrow yexception() << "incorrect fields parameter";

        return true;
    }

    void PrepareConfig(TUnstrictConfig& result, const char* configStr) {
        if (!result.ParseMemory(configStr)) {
            TString errors;
            result.PrintErrors(errors);
            ythrow yexception() << "errors in server config: " << errors;
        }
    }
} // namespace

namespace NControllerStatusGuardService {
    TMutex Mutex;
}

namespace {
    class TSystemInfoMetrics : public TCompositeMetric {
    private:
        TPart<TOrangeMetric> VirtualMemory;
        TPart<TOrangeMetric> ResidentSetSize;
        TPart<TOrangeMetric> MajorPageFaults;
    public:
        TSystemInfoMetrics()
            : VirtualMemory(this, GetMetricsPrefix() + "System_VirtualMemory")
            , ResidentSetSize(this, GetMetricsPrefix() + "System_ResidentSetSize")
            , MajorPageFaults(this, GetMetricsPrefix() + "System_MajorPageFaults")
        {
        }
        static void UpdateInfo() {
            auto mi = NMemInfo::GetMemInfo();
            auto ru = TRusage::Get();

            auto instance = Singleton<TAutoGlobal<TSystemInfoMetrics>>();
            instance->MajorPageFaults.Set(ru.MajorPageFaults);
            instance->VirtualMemory.Set(mi.VMS);
            instance->ResidentSetSize.Set(mi.RSS);
        }
    };

    TString GetConfigDir(const TServerConfigConstructorParams& configParams) {
        return TFsPath(configParams.Path).Parent().GetPath();
    }
}

struct TController::TConfigHashes {
    void Reset(const TString& configPath) {
        try {
            const TInstant start = TInstant::Now();
            INFO_LOG << "Calculating config hashes..." << Endl;
            auto hashes = GetDirHashes(configPath);
            {
                TWriteGuard g(Mutex);
                Hashes = std::move(hashes);
                ConfigPath = configPath;
            }
            const TDuration duration = TInstant::Now() - start;
            INFO_LOG << "Calculated config hashes, time = " << duration.MicroSeconds() << "us" << Endl;
        } catch (...) {
            ERROR_LOG << "Cannot calculate config hashes, config path = " << configPath << Endl;
        }
    }

    void UpdateEntry(const TString& configPath, const TString& entryRelativePath, const TFileHashInfo& info) {
        TWriteGuard g(Mutex);
        if (IsTheSameConfigDir(configPath)) {
            Hashes[entryRelativePath] = info;
        }
    }

    TDirHashInfo GetHashes(const TString& configPath, const TRegExMatch* filter = nullptr) const {
        TDirHashInfo result;
        bool noCache = true;
        {
            TReadGuard g(Mutex);
            if (IsTheSameConfigDir(configPath)) {
                result = FilterDirHashes(Hashes, filter);
                noCache = false;
            }
        }
        if (noCache) {
            result = GetDirHashes(configPath, filter);
        }
        return result;
    }

private:
    bool IsTheSameConfigDir(const TString& configPath) const {
        return !ConfigPath.empty() && configPath == ConfigPath;
    }

    TRWMutex Mutex;
    TDirHashInfo Hashes;
    TString ConfigPath;
};

class TControllerStatusGuard {
private:
    TString FileName;
    bool ReadOnly;
    NController::TControllerStatus Status;

public:
    const NController::TControllerStatus& GetStatus() const {
        return Status;
    }

    NController::TControllerStatus& MutableStatus() {
        VERIFY_WITH_LOG(!ReadOnly, "invalid usage of TControllerStatusGuard");
        return Status;
    }

    TControllerStatusGuard(const TString& fileName, bool readOnly = false)
        : FileName(fileName)
        , ReadOnly(readOnly)
    {
        TGuard<TMutex> g(NControllerStatusGuardService::Mutex);
        try {
            CHECK_WITH_LOG(!!FileName);
            if (TFsPath(fileName).Exists()) {
                TUnbufferedFileInput fi(fileName);
                TString text = fi.ReadAll();
                CHECK_WITH_LOG(::google::protobuf::TextFormat::ParseFromString(text, &Status));
            } else {
                Status.SetStatus(NController::OK);
            }
        } catch (...) {
            ERROR_LOG << "Can't read status file(" << fileName << "): " << CurrentExceptionMessage() << Endl;
        }
    }

    ~TControllerStatusGuard() {
        if (!FileName || ReadOnly)
            return;
        TGuard<TMutex> g(NControllerStatusGuardService::Mutex);
        try {
            TString result;
            CHECK_WITH_LOG(::google::protobuf::TextFormat::PrintToString(Status, &result));
            TUnbufferedFileOutput fo(FileName);
            fo << result;
            NOTICE_LOG << "write status file(" << FileName << "): " << result << Endl;
        } catch (...) {
            ERROR_LOG << "Can't write status file(" << FileName << "): " << CurrentExceptionMessage() << Endl;
        }
    }

    void Release() {
        FileName = "";
    }

};

//TController
bool TController::Process(IMessage* message) {
    TCollectServerInfo* msg = dynamic_cast<TCollectServerInfo*>(message);
    if (msg) {
        TInstant now = Now();
        msg->Fields["controller_uptime"] = (now - ControllerStart).Seconds();
        msg->Fields["controller_status"] = ToString(GetStatus());
        msg->Fields["server_uptime"] = ServerStart != TInstant::Zero() ? ToString(now - ServerStart) : "0";
        msg->Fields["nprofile_enabled"] = NProfile::IsEnabledGlobal();
        TUnstrictConfig::ToJson(Descriptor.GetConfigString(*this), msg->Fields["config"]);
        return true;
    }
    TSystemStatusMessage* msgStatusData = dynamic_cast<TSystemStatusMessage*>(message);
    if (msgStatusData) {
        SetServerDataStatus(msgStatusData->GetMessage(), msgStatusData->GetStatus());
        return true;
    }
    TCollectMetricsMessage* collectMetrics = dynamic_cast<TCollectMetricsMessage*>(message);
    if (collectMetrics) {
        TSystemInfoMetrics::UpdateInfo();
        return true;
    }
    if (auto msg = message->As<TUpdateConfigHash>()) {
        const auto configDir = GetConfigDir(ConfigParams);
        const TFsPath entryPath = msg->Path;
        if (entryPath.IsSubpathOf(configDir)) {
            ConfigHashes->UpdateEntry(configDir, entryPath.RelativeTo(configDir), msg->Info);
        }
        return true;
    }
    return false;
}

TServerInfo TController::GetServerInfo(bool controllerOnly, bool isHumanReadable) {
    TMap<TString, TString> statusInfo;
    {
        TControllerStatusGuard csg(GetDaemonConfig()->GetController().GetStatusFileName(), true);
        statusInfo["state"] = NController::TSlotStatus_Name(csg.GetStatus().GetStatus());
        statusInfo["info"] = ToString(csg.GetStatus().GetInfo());
    }
    statusInfo["timestamp"] = ToString(Now().GetValue());
    if (!controllerOnly) {
        THolder<TCollectServerInfo> msg(Descriptor.CreateInfoCollector(isHumanReadable));
        TServerInfo info = CollectServerInfo(*msg);
        info("server_status_global", statusInfo);
        return info;
    } else {
        TServerInfo info;
        info("server_status_global", statusInfo);
        return info;
    }
}

TString TController::GetConfigDirMD5() const {
    return DirDigest(GetDaemonConfig()->GetController().ConfigsRoot.c_str());
}

void TController::StoreConfigs() const {
    if (!ConfigParams.Daemon->GetController().ConfigsControl)
        return;
    TGuardIncompatibleAction g(ConfigMutex);
    TFsPath from(ConfigParams.Daemon->GetController().ConfigsRoot);
    TFsPath to(ConfigParams.Daemon->GetController().StateRoot + "/last_success");
    to.ForceDelete();
    try {
        NUtil::CopyRecursive(from, to, nullptr, false);
    } catch (...) { // Logged already
        to.ForceDelete();
    }
}

bool TController::RestoreConfigs() const {
    if (!ConfigParams.Daemon->GetController().ConfigsControl) {
        NOTICE_LOG << "Configs control is switched off" << Endl;
        return false;
    }
    TGuardIncompatibleAction g(ConfigMutex);
    TFsPath from(GetDaemonConfig()->GetController().StateRoot + "/last_success");
    if (from.Exists()) {
        TFsPath to(GetDaemonConfig()->GetController().ConfigsRoot);
        to.ForceDelete();
        NUtil::CopyRecursive(from, to);
        return true;
    } else {
        NOTICE_LOG << "Did not find correct configs in " << from.GetPath() << Endl;
        return false;
    }
}

void TController::RestartServer(ui32 rigidStopLevel, const TCgiParameters* cgiParams, bool reread, TRestartServerStatistics* statistics, const TDuration& sleepTimeout) {
    TGuardTransaction g(ServerTransaction);
    try {
        THolder<TGuardIncompatibleAction> cg(new TGuardIncompatibleAction(ConfigMutex));
        bool newConfigsLoaded = false;
        if (FirstStart && ConfigParams.Daemon->GetController().DMOptions.Enabled) {
            NOTICE_LOG << "Data restoring from DM..." << Endl;
            newConfigsLoaded = DownloadConfigsFromDM(ConfigParams.Daemon->GetController().DMOptions);
            if (newConfigsLoaded)
                NOTICE_LOG << "Data restoring from DM...OK" << Endl;
            else
                NOTICE_LOG << "Data restoring from DM...FAILED" << Endl;
        }
        bool lastConfigsRestored = false;

         {
            const TString configsMD5 = GetConfigDirMD5();
            TControllerStatusGuard csg(GetDaemonConfig()->GetController().GetStatusFileName(), true);
            const NController::TControllerStatus& controllerStatus = csg.GetStatus();

            if (controllerStatus.GetStatus() == NController::FailedConfig && controllerStatus.GetConfigDirMD5() == configsMD5) {
                WARNING_LOG << "Server's configs restoring..." << Endl;
                if (!RestoreConfigs()) {
                    WARNING_LOG << "Need in correct configs" << Endl;
                    SetStatus(NController::ssbcNotRunnable);
                    return;
                }
                lastConfigsRestored = true;
            }
        }
        reread |= FirstStart | newConfigsLoaded | lastConfigsRestored;
        if (reread) {
            VERIFY_WITH_LOG(!!ConfigParams.Path, "There is no path in configParams");
            if (!TFsPath(ConfigParams.Path).Exists()) {
                WARNING_LOG << "Server's configs removed. Restoring..." << Endl;
                VERIFY_WITH_LOG(RestoreConfigs(), "can't find config %s", ConfigParams.Path.data());
                WARNING_LOG << "Server's configs removed. Restoring... OK" << Endl;
            }
            VERIFY_WITH_LOG(TFsPath(ConfigParams.Path).Exists(), "Incorrect filename: %s", ConfigParams.Path.data());
            if (ConfigParams.Preprocessor) {
                try {
                    ConfigParams.Preprocessor->ReReadEnvironment();
                    ConfigParams.Text = ConfigParams.Preprocessor->ReadAndProcess(ConfigParams.Path);
                } catch (...) {
                    const TString& error = CurrentExceptionMessage();
                    AbortFromCorruptedConfig("Preprocessor error: %s", error.data());
                }
            } else
                ConfigParams.Text = TUnbufferedFileInput(ConfigParams.Path).ReadAll();
            cg.Reset(nullptr);
            Reset(rigidStopLevel, cgiParams, statistics, sleepTimeout, true);
            cg.Reset(new TGuardIncompatibleAction(ConfigMutex));
        } else {
            DestroyServer(rigidStopLevel, cgiParams, statistics, sleepTimeout);
        }
        NOTICE_LOG << "Create and start Server..." << Endl;
        CheckConfig(*Config, true);
        if (ConfigParams.Daemon->GetController().DMOptions.Enabled) {
            ConfigHashes->Reset(GetConfigDir(ConfigParams));
        }
        Singleton<TRTYPools>()->Initialize(ConfigParams.Daemon->GetController().Threading);
        TInstant serverStarting = Now();
        SetStatus(NController::ssbcStarting);
        Server.Reset(new TExtendedServer(Descriptor.CreateServer(*Config), *Config));

        NController::TSlotStatus dataStatus = GetServerDataStatus();
        if (dataStatus == NController::FailedIndex || dataStatus == NController::UnknownError) {
            WARNING_LOG << "Server is not runnable for " << ConfigParams.Path << Endl;
            SetStatus(NController::ssbcNotRunnable);
            Server.Reset(nullptr);
            return;
        }
        Server->Run();
        StoreConfigs();
        SetStatus(NController::ssbcActive);
        ServerStart = Now();
        TDuration startTime = ServerStart - serverStarting;
        if (statistics)
            statistics->StartTimeMilliseconds = startTime.MilliSeconds();
        NOTICE_LOG << "Create and start Server...OK (" << startTime.MilliSeconds() << " ms)" << Endl;
    } catch (...) {
        const TString& message = CurrentExceptionMessage();
        FAIL_LOG("Cannot restart server: %s", message.data());
    }
}

void TController::DestroyServer(ui32 rigidStopLevel, const TCgiParameters* cgiParams, TRestartServerStatistics* statistics, const TDuration& sleepTimeout) {
    TGuardTransaction g(ServerTransaction);
    if (!!Server) {
        SetStatus(NController::ssbcStopping);
        ui64 stopTime = millisec();
        NOTICE_LOG << "Server exists. Stopping it..." << Endl;
        if (sleepTimeout.GetValue()) {
            NOTICE_LOG << "Sleep..." << Endl;
            Sleep(sleepTimeout);
            NOTICE_LOG << "Sleep...OK" << Endl;
        }
        StopServer(rigidStopLevel, cgiParams);
        NOTICE_LOG << "Stop server time " << (millisec() - stopTime) << Endl;
        Server.Reset(nullptr);
        stopTime = millisec() - stopTime;
        if (statistics)
            statistics->StopTimeMilliseconds = stopTime;
        SetStatus(NController::ssbcStopped);
        NOTICE_LOG << "Stop and null server time " << stopTime << Endl;
        NOTICE_LOG << "Server exists. Stopping it...OK (" << stopTime << " ms)" << Endl;
    }
    else if (statistics)
        statistics->StopTimeMilliseconds = 0;
    Singleton<TRTYPools>()->Destroy();
}

void TController::Reset(ui32 rigidStopLevel, const TCgiParameters* cgiParams, TRestartServerStatistics* statistics, const TDuration& sleepTimeout, bool checkConfig) {
    DestroyServer(rigidStopLevel, cgiParams, statistics, sleepTimeout);
    try {
        TAtomicSharedPtr<IServerConfig> newConfig(Descriptor.CreateConfig(ConfigParams));
        TDaemonConfig newDaemon(ConfigParams.Text.data(), false);
        if (checkConfig)
            CheckConfig(*newConfig, false);
        TGuardTransaction g(ConfigMutex);
        Config.Swap(newConfig);
        if (ConfigParams.Daemon->ToString("") != newDaemon.ToString("")) {
            if (newDaemon.GetController().ReinitLogsOnRereadConfigs)
               newDaemon.InitLogs();
            *ConfigParams.Daemon = newDaemon;
        }
    } catch(...) {
        TString error = CurrentExceptionMessage();
        AbortFromCorruptedConfig("Error on reset: %s", error.data());
    }
    FirstStart = false;
}

void TController::Stop(ui32 rigidStopLevel) {
    DestroyServer(rigidStopLevel);
    if (GetDaemonConfig()->GetController().Enabled) {
        try {
            THttpServer::Shutdown();
            Wait();
            AsyncExecuter.Stop();
        } catch (...) {
            ERROR_LOG << "Error in TController::Stop " << CurrentExceptionMessage() << Endl;
        }
    }
    Stopped.Signal();
}

void TController::Run() {
    ThreadDisableBalloc();
    if (ConfigParams.Daemon->GetController().EnableNProfile) {
        NProfile::EnableGlobal();
    } else {
        NProfile::DisableGlobal();
    }
    if (ConfigParams.Daemon->GetController().Enabled) {
        AsyncExecuter.Start();
        if (!Start()) {
            FAIL_LOG("cannot start http server for controller, error text %s", GetError());
        }
    }
    if (ConfigParams.Daemon->GetController().StartServer)
        RestartServer(false);
    else
        NOTICE_LOG << "Controller started. Server will start later" << Endl;
    if (ConfigParams.Daemon->GetController().AutoStop)
        Stop(0);
}

void TController::FillConfigsHashes(NJson::TJsonValue& result, TString configPath, const TRegExMatch* filter) const {
    if (!configPath)
        configPath = GetConfigDir(ConfigParams);
    TDirHashInfo info = ConfigHashes->GetHashes(configPath, filter);
    for (const auto& i : info)
        result.InsertValue(i.first, i.second.Hash);
}

bool TController::CanSetConfig(bool& isExists) {
    TGuardServer gs = GetServer();
    isExists = !!gs;
    if (!isExists)
        return false;
    return Descriptor.CanSetConfig(gs);
}

NController::TSlotStatus TController::GetServerDataStatus() const {
    TControllerStatusGuard csg(GetDaemonConfig()->GetController().GetStatusFileName(), true);
    return csg.GetStatus().GetStatus();
}

bool TController::ClearServerDataStatus(bool force) const {
    return SetServerDataStatus("", TSystemStatusMessage::ESystemStatus::ssOK, force);
}

bool TController::SetServerDataStatus(const TString& info, const TSystemStatusMessage::ESystemStatus status, bool force) const {
    if (GetDaemonConfig()->IsStatusControlEnabled() || force) {
        TControllerStatusGuard csg(GetDaemonConfig()->GetController().GetStatusFileName(), false);

        NController::TSlotStatus statusNew = NController::TSlotStatus::UnknownError;
        if (status == TSystemStatusMessage::ESystemStatus::ssIncorrectConfig) {
            statusNew = NController::TSlotStatus::FailedConfig;
        } else if (status == TSystemStatusMessage::ESystemStatus::ssIncorrectData) {
            statusNew = NController::TSlotStatus::FailedIndex;
        } else if (status == TSystemStatusMessage::ESystemStatus::ssSemiOK) {
            statusNew = NController::TSlotStatus::SemiOK;
        } else if (status == TSystemStatusMessage::ESystemStatus::ssOK) {
            statusNew = NController::TSlotStatus::OK;
        }

        const TString configMD5 = GetConfigDirMD5();
        if (statusNew != csg.GetStatus().GetStatus() || info != csg.GetStatus().GetInfo() || csg.GetStatus().GetConfigDirMD5() != configMD5) {
            FATAL_LOG << "InfoServerSignal : " << info << "/" << NController::TSlotStatus_Name(statusNew) << "/" << configMD5 << Endl;
            csg.MutableStatus().SetStatus(statusNew);
            csg.MutableStatus().SetInfo(info);
            csg.MutableStatus().SetConfigDirMD5(configMD5);
        } else {
            csg.Release();
        }
    }

    return true;
}

bool TController::SetConfigFields(const TCgiParameters& cgiParams, NJson::TJsonValue& result) {
    TString prefix;
    NJson::TJsonValue config;
    if (!ParseFieldParam(cgiParams, config, prefix)) {
        return false;
    };
    const NJson::TJsonValue::TMapType* values;
    if (!config.GetMapPointer(&values))
        ythrow yexception() << "incorrect fields parameter";

    TUnstrictConfig serverConfig;
    TString confTextOld = Descriptor.GetConfigString(*this);
    PrepareConfig(serverConfig, confTextOld.data());
    bool configChanged = false;
    for (NJson::TJsonValue::TMapType::const_iterator i = values->begin(), e = values->end(); i != e; ++i) {
        const TString& path = i->first;
        const TString& value = i->second.GetString();
        bool fieldChanged = serverConfig.PatchEntry(path, value, prefix);
        configChanged |= fieldChanged;
        NJson::TJsonValue& fieldVal = result.InsertValue(path, NJson::JSON_MAP);
        fieldVal.InsertValue("value", value);
        fieldVal.InsertValue("result", fieldChanged);
    }
    if (!configChanged)
        return true;

    TStringStream configStrNew;
    serverConfig.PrintConfig(configStrNew);
    TGuardTransaction g(ConfigMutex);
    ConfigParams.Text = configStrNew.Str();

    Reset(0);
    return  true;
}

bool TController::GetConfigFields(const TCgiParameters& cgiParams, NJson::TJsonValue& result){

    TString prefix;
    NJson::TJsonValue fields;
    if (!ParseFieldParam(cgiParams, fields, prefix)) {
        return false;
    };

    const NJson::TJsonValue::TArray* values;
    if (!fields.GetArrayPointer(&values))
        ythrow yexception() << "incorrect fields parameter";

    TUnstrictConfig serverConfig;
    TString confText = Descriptor.GetConfigString(*this);
    PrepareConfig(serverConfig, confText.data());

    for (NJson::TJsonValue::TArray::const_iterator i = values->begin(), e = values->end(); i != e; ++i) {
        const TString& path = i->GetString();
        const TYandexConfig::Section* section = serverConfig.GetSection(prefix + path);
        if (section)
            TUnstrictConfig::ToJsonPatch(*section, result, path);
        else
            result.InsertValue(path, serverConfig.GetValue(prefix + path));
    }
    return true;
}

bool TController::DownloadConfigsFromDM(const TDaemonConfig::TControllerConfig::TDMOptions& dm, const TString& version, const NSaas::TSlotInfo* slotInfo) {
    TFsPath tempConfDir = TFsPath(ConfigParams.Daemon->GetController().ConfigsRoot).Parent() / "configs_new";
    tempConfDir.ForceDelete();
    TInstant deadline = Now() + dm.Timeout;
    NDaemonController::TControllerAgent agent(dm.Host, dm.Port, nullptr, dm.UriPrefix);
    TString tags = NSaasServerInfo::GetPseudoTagsString();
    NDaemonController::TListConfAction listConf(dm.Slot, dm.Service, dm.ServiceType, dm.CType, dm.Attemptions, version, slotInfo, deadline - Now(), tags);
    if (!agent.ExecuteAction(listConf))
        return false;

    const TString& downloadedService = listConf.GetSlotInfoResult() ? listConf.GetSlotInfoResult()->Service : Default<TString>();
    const TString& dmService = slotInfo ? slotInfo->Service : dm.Service;
    if (downloadedService) {
        if (dmService && dmService != downloadedService)
            ythrow yexception() << "downloaded wrong service " << downloadedService << ", must be " << dmService;
        if (!dmService && downloadedService == "unused") {
            WARNING_LOG << "maybe slot not recognized" << Endl;
            MustByAlive = false;
            return false;
        }
    }

    NJson::TJsonValue fileVersions;
    for (auto& file : listConf.GetFileList()) {
        TInstant startAction = Now();
        if (startAction >= deadline) {
            WARNING_LOG << "Can't receive actual config files from DM: will try to start previous configuration" << Endl;
            return false;
        }
        INFO_LOG << "Try to get file : url=" << file.Url << ", filename=" << file.Name << "..." << Endl;
        NDaemonController::TGetConfAction getConf(file, deadline - startAction, dm.Attemptions);
        if (!agent.ExecuteAction(getConf))
            return false;
        file = getConf.GetData();
        INFO_LOG << "Try to get file : url=" << file.Url << ", filename=" << file.Name << "...OK" << Endl;

        fileVersions[file.Name] = file.Version;
    }
    for (auto& file : listConf.GetFileList())
        WriteFile(file.Name, file.Content, tempConfDir);

    if (!ConfigParams.Daemon->GetController().VersionsFileName.empty()) {
        WriteFile(ConfigParams.Daemon->GetController().VersionsFileName, NJson::WriteJson(fileVersions, true /*formatOutput*/, false /*sortkeys*/, false /*validateUtf8*/), tempConfDir);
    }

    TFsPath(ConfigParams.Daemon->GetController().ConfigsRoot).ForceDelete();
    tempConfDir.RenameTo(ConfigParams.Daemon->GetController().ConfigsRoot);
    return true;
}

void TController::CreatePath(const TString& object) {
    if (!object) {
        return;
    }

    const TFsPath path(object);
    const TFsPath directory = path.IsDirectory() ? path : path.Parent();
    if (!directory.Exists()) {
        directory.MkDirs();
    }
}

void TController::LockSelfMemory() noexcept {
    try {
        NYT::MlockFileMappings();
    } catch (...) {
        WARNING_LOG << "Cannot lock self memory" << Endl;
    }
}

TController::TController(TServerConfigConstructorParams& params, const IServerDescriptor& descriptor)
    : THttpServer(this, params.Daemon->GetController())
    , ConfigParams(params)
    , Status(NController::ssbcStopped)
    , ControllerStart(Now())
    , AsyncExecuter(params.Daemon->GetController().nThreads)
    , Log(CreateLogBackend(params.Daemon->GetController().Log, LOG_MAX_PRIORITY, /* threaded = */ true))
    , NoFileOperations(false)
    , Descriptor(descriptor)
    , FirstStart(true)
    , MustByAlive(true)
    , ConfigHashes(MakeHolder<TConfigHashes>())
{
    if (params.Daemon->ShouldLockExecutablePages()) {
        LockSelfMemory();
    }

    RegisterGlobalMessageProcessor(this);

    CreatePath(params.Daemon->GetController().ConfigsRoot);
    CreatePath(params.Daemon->GetController().StateRoot);
    CreatePath(params.Daemon->GetMetricsStorage());

    SetMetricsPrefix(params.Daemon->GetMetricsPrefix());
    SetMetricsMaxAgeDays(params.Daemon->GetMetricsMaxAge());
    SetPersistentMetricsStorage(params.Daemon->GetMetricsStorage());

    if (!ConfigParams.Daemon->GetController().DMOptions.Enabled) {
        Reset(0);
    }
}

TController::~TController() {
    auto start = Now();
    while (Now() - start < TDuration::Minutes(2)) {
        if (!AtomicGet(ClientsCount)) {
            break;
        }
        Sleep(TDuration::MilliSeconds(10));
    }
    DestroyServer(1);
    UnregisterGlobalMessageProcessor(this);
}

TController::TClient::TClient(TController& owner)
    : Owner(owner)
{
    AtomicIncrement(Owner.ClientsCount);
}

bool TController::TClient::Reply(void* /*ThreadSpecificResource*/) {
    ThreadDisableBalloc();
    try {
        ProcessHeaders();
        RD.Scan();
        LogRequest();
        if (ProcessSpecialRequest()) {
            return true;
        }

        const TString& command = RD.CgiParam.Get("command");
        TController::TCommandProcessor::TPtr worker(Owner.Descriptor.CreateCommandProcessor(command));
        if (!worker) {
            Output() << "HTTP/1.1 501 Not Implemented\r\n\r\n";
            return true;
        }
        bool async = RD.CgiParam.Has("async") && FromString<bool>(RD.CgiParam.Get("async"));
        worker->Init(Owner, this, async);
        if (async) {
            NJson::TJsonValue result;
            Owner.ExecuteAsyncCommand(worker.Release(), result);
            if (PostBuffer().Size())
                result.InsertValue("content_hash", MD5::Calc(TStringBuf(PostBuffer().AsCharPtr(), PostBuffer().Size())));
            TString reply = NJson::WriteJson(result, true, true, false);
            Output() << "HTTP/1.1 202 Data Accepted\r\n"
                "Content-Length:" << reply.size() << "\r\n"
                "\r\n"
                << reply;
            return true;
        }
        worker->Process(nullptr);
        return true;
    } catch (...) {
        ERROR_LOG << "Error while TController::TClient::Reply " << CurrentExceptionMessage() << Endl;
        try {
            Output() << "HTTP/1.1 500 Internal Server Error\r\n\r\n" << CurrentExceptionMessage();
        } catch (...) { } // suppress error if the socket is broken
    }
    return true;
}

void TController::TClient::LogRequest() {
    if (IsTrue(RD.CgiParam.Get("tire_req"))) {
        return;
    }

    NUtil::TTSKVRecord record("controller-incoming");
    const TInstant now = Now();
    record.Add("ts", now.Seconds());
    record.Add("time", now.ToString());
    record.Add("ip", RD.RemoteAddr());
    record.Add("script", RD.ScriptName());
    record.Add("params", RD.Query());
    record.Add("post-size", PostBuffer().Size());
    Owner.GetLog() << record.ToString() << Endl;
}

//TController::TCommandProcessor

void TController::TCommandProcessor::Init(TController& owner, TController::TClient* requester, bool async) {
    Async = async;
    Owner = &owner;
    if (Async) {
        Requester = nullptr;
        Cgi = requester->GetRD().CgiParam;
        Buf.Assign(requester->PostBuffer().AsCharPtr(), requester->PostBuffer().Size());
    }
    else
        Requester = requester;
}

void TController::TCommandProcessor::Process(void* /*ts*/) {
    ThreadDisableBalloc();
    SetStatus(stsStarted);
    TStringBuf data = GetPostBuffer();
    if (!Async && !!data)
        Write("content_hash", MD5::Calc(data));
    Write("command", GetName());
    Write("description", GetInfoMessage());
    const TString timeoutStr = GetCgi().Get("timeout");
    TDuration timeout;
    if (!!timeoutStr)
        timeout = TDuration::Parse(timeoutStr);
    TBomb bomb(timeout, "process commmand " + GetName() + " too long");
    bool success = true;
    try {
        DoProcess();
        SetStatus(stsFinished);
    }
    catch (const yexception& e) {
        Write("error", e.what());
        ERROR_LOG << "Error while processing command " << GetName() << ": " << e.what() << Endl;
        success = false;
        SetStatus(stsFailed, e.what());
    }
    if (!Async) {
        TString replyStr = NJson::WriteJson(*GetReply(), true, true, false);
        TStringStream ss;
        ss << "HTTP/1.1 " << (success ? 200 : 400) << " Ok\r\nContent-Type: text/json\r\nContent-Length: " << replyStr.size() << "\r\n\r\n"
            << replyStr;
        Requester->Output().Write(ss.Str());
        Requester->Output().Finish();
    };
}

TController::TGuardServer TController::TCommandProcessor::GetServer(bool mustBe) {
    TController::TGuardServer server = Owner->GetServer();
    if (mustBe && !server)
        ythrow yexception() << "No server";
    return server;
}
