#include "daemon_config.h"
#include "thread_opts.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/util/logging/tskv_log.h>

#include <library/cpp/logger/global/global.h>
#include <library/cpp/yconf/patcher/config_patcher.h>
#include <library/cpp/yconf/patcher/unstrict_config.h>

#include <util/datetime/base.h>
#include <util/generic/ptr.h>
#include <util/generic/string.h>
#include <util/generic/yexception.h>
#include <util/folder/dirut.h>
#include <util/folder/path.h>
#include <util/stream/file.h>
#include <util/string/cast.h>
#include <util/string/vector.h>
#include <util/system/execpath.h>
#include <util/system/hostname.h>

#if defined(_unix_)
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
#else
    #define STDOUT_FILENO INVALID_FHANDLE
    #define STDERR_FILENO INVALID_FHANDLE
#endif

const TYandexConfig::Section& GetSubSection(
    const TYandexConfig::Section& section,
    const TString& name)
{
    const TYandexConfig::TSectionsMap& children = section.GetAllChildren();
    const TYandexConfig::TSectionsMap::const_iterator iter =
        children.find(name);
    if (iter == children.end()) {
        ythrow yexception() << "Section " << name.Quote() << " was not found";
    }
    return *iter->second;
}

void TDaemonConfig::InitLogs() {
    DoInitGlobalLog(LoggerType, LogLevel, LogRotation, StartAsDaemon(), /* formatter = */ {}, /* threaded = */ true);
    if (!!StdOut && !NFs::Exists(TFsPath(StdOut).Parent().GetPath()))
        ythrow yexception() << "directory with stdout redirect file is incorrect: " << TFsPath(StdOut).Parent().GetPath();

    if (!!StdErr && !NFs::Exists(TFsPath(StdErr).Parent().GetPath()))
        ythrow yexception() << "directory with stderr redirect file is incorrect: " << TFsPath(StdErr).Parent().GetPath();

    ReplaceDescriptor(StdOut, STDOUT_FILENO);
    ReplaceDescriptor(StdErr, STDERR_FILENO);

    NUtil::TTSKVRecord::SetStdIName(GetLoggerIName());
}

TDaemonConfig::TDaemonConfig(const char* config, bool doInitLogs)
    : MetricsMaxAge(3)
{
    TAnyYandexConfig parsedConfig;
    CHECK_WITH_LOG(parsedConfig.ParseMemory(config));
    const TYandexConfig::Section* ServerSection = &GetSubSection(*parsedConfig.GetRootSection(), "DaemonConfig");
    const TYandexConfig::Directives& directives =
        ServerSection->GetDirectives();
    if (directives.find("PidFile") != directives.end()) {
        directives.GetValue("PidFile", PidFile);
    }

    if (directives.find("MetricsPrefix") != directives.end()) {
        directives.GetValue("MetricsPrefix", MetricsPrefix);
    }
    if (directives.find("MetricsMaxAge") != directives.end()) {
        directives.GetValue("MetricsMaxAge", MetricsMaxAge);
    }
    if (directives.find("MetricsStorage") != directives.end()) {
        directives.GetValue("MetricsStorage", MetricsStorage);
    }

    directives.GetValue("LoggerType", LoggerType);
    directives.GetValue("LoggerIName", LoggerIName);
    LogRotation = directives.Value("LogRotation", false);
    LockExecutablePages = directives.Value("LockExecutablePages", true);
    EnableStatusControl = directives.Value("EnableStatusControl", true);

    directives.GetValue("SpecialProcessors", SpecialProcessors);
    TVector<TString> processorsInfo = SplitString(SpecialProcessors, ";");
    for (auto i : processorsInfo) {
        if (i.size()) {
            TVector<TString> info = SplitString(i, ":");
            if (info.size() == 0)
                WARNING_LOG << "Incorrect processor info for " << i << Endl;
            else if (info.size() == 1) {
                SpecialProcessorsMap[info[0]] = "";
            } else if (info.size() == 2) {
                SpecialProcessorsMap[info[0]] = info[1];
            } else {
                SpecialProcessorsMap[info[0]] = JoinStrings(info, 1, info.size() - 1, ":");
            }
        }
    }
    if (!directives.GetValue("LogLevel", LogLevel)) {
        ythrow yexception() << "LogLevel was not set";
    }

    directives.GetValue("LogQueueSize", LogQueueSize);
    directives.GetValue("StdOut", StdOut);
    directives.GetValue("StdErr", StdErr);
    if (doInitLogs)
        InitLogs();

    TYandexConfig::TSectionsMap sections = ServerSection->GetAllChildren();
    const TYandexConfig::TSectionsMap::const_iterator iter = sections.find("Controller");
    if (iter != sections.end())
        Controller.Init(*iter->second);
}

TString TDaemonConfig::ToString(const char* sectionName) const {
    TStringStream so;
    so << "<" << sectionName << ">" << Endl;
    so << "PidFile:" << PidFile << Endl;
    so << "MetricsPrefix:" << MetricsPrefix << Endl;
    so << "MetricsMaxAge:" << MetricsMaxAge << Endl;
    so << "LogLevel:" << LogLevel << Endl;
    so << "LogQueueSize:" << LogQueueSize << Endl;
    so << "LogRotation:" << LogRotation << Endl;
    so << "LockExecutablePages:" << LockExecutablePages << Endl;
    so << "EnableStatusControl:" << EnableStatusControl << Endl;
    so << "SpecialProcessors: " << SpecialProcessors << Endl;
    so << "LoggerType:" << LoggerType << Endl;
    so << "LoggerIName:" << LoggerIName << Endl;
    so << "StdOut:" << StdOut << Endl;
    so << "StdErr:" << StdErr << Endl;
    so << Controller.ToString("Controller");
    so << "</" << sectionName << ">" << Endl;
    return so.Str();
}

void TDaemonConfig::ReplaceDescriptor(const TString& name, FHANDLE fd) const
{
    if (!name || name == "console"sv || name == "null"sv)
        return;

    TFileHandle stream(fd);
    CHECK_WITH_LOG(stream.LinkTo(TFileHandle(name, OpenAlways | WrOnly | ForAppend | AWUser | ARUser | ARGroup | AROther)));
    stream.Release();
}


void TDaemonConfig::ReopenLog() const
{
    ReplaceDescriptor(StdOut, STDOUT_FILENO);
    ReplaceDescriptor(StdErr, STDERR_FILENO);
}

TDaemonConfig::TNetworkAddress TDaemonConfig::ParseAddress(
    const TYandexConfig::Directives& directives, const TString& prefix,
    const TString& defaultHost)
{
    TString host;
    if (!directives.GetValue(prefix + "Host", host)) {
        if (!!defaultHost)
            host = defaultHost;
    }

    ui16 port = ui16();
    if (!directives.GetValue(prefix + "Port", port)) {
        ythrow yexception() << "Directive " << (prefix + "Port").Quote()
            << " wasn't found";
    }

    return std::make_pair(host, port);
}

template <class TType>
static inline bool GetValue(const TYandexConfig::Directives& directives,
    const TString& key, TType& value)
{
    if (directives.find(key) != directives.end()) {
        return directives.GetValue(key, value);
    } else {
        return false;
    }
}

static inline bool GetValue(const TYandexConfig::Directives& directives,
    const TString& key, TDuration& value)
{
    ui64 intervalMs = 0;
    if (directives.find(key) == directives.end() || !directives.GetValue(key, intervalMs)) {
        return false;
    }
    value = TDuration::MilliSeconds(intervalMs);
    return true;
}

template <class TType>
static inline TType Value(const TYandexConfig::Directives& directives,
    const TString& key, TType def)
{
    if (directives.find(key) != directives.end()) {
        return directives.Value(key, def);
    } else {
        return def;
    }
}

THttpServerOptions TDaemonConfig::ParseHttpServerOptions(
    const TYandexConfig::Directives& directives, const TString& prefix,
    const TString& defaultHost)
{
    TNetworkAddress address = ParseAddress(directives, prefix, defaultHost);
    THttpServerOptions options(address.second);
    options.Host = address.first;
    options.MaxConnections = 0;
    GetValue(directives, prefix + "Threads", options.nThreads);
    GetValue(directives, prefix + "MaxQueueSize", options.MaxQueueSize);
    GetValue(directives, prefix + "MaxFQueueSize", options.MaxFQueueSize);
    GetValue(directives, prefix + "MaxConnections", options.MaxConnections);
    GetValue(directives, prefix + "KeepAliveEnabled", options.KeepAliveEnabled);
    if (!GetValue(directives, prefix + "FThreads", options.nFThreads)) {
        options.nFThreads = ClampVal<ui32>(options.nFThreads /*=1*/, 1 + options.MaxFQueueSize / 32, ClampVal<ui32>(options.nThreads, 1, 4));
    }
    GetValue(directives, prefix + "ClientTimeout", options.ClientTimeout);
    GetValue(directives, prefix + "CompressionEnabled", options.CompressionEnabled);
    GetValue(directives, prefix + "RejectExcessConnections", options.RejectExcessConnections);
    GetValue(directives, prefix + "ReusePort", options.ReusePort);
    GetValue(directives, prefix + "ReuseAddress", options.ReuseAddress);
    GetValue(directives, prefix + "ListenBacklog", options.ListenBacklog);
    GetValue(directives, prefix + "OutputBufferSize", options.OutputBufferSize);
    GetValue(directives, prefix + "PollTimeoutMs", options.PollTimeout);
    GetValue(directives, prefix + "ExpirationTimeoutMs", options.ExpirationTimeout);

    return options;
}

TDaemonConfig::TConnection TDaemonConfig::ParseConnection(
    const TYandexConfig::Directives& directives, const TString& prefix, TDuration connectionTimeout, TDuration interactionTimeout)
{
    TNetworkAddress address = ParseAddress(directives, prefix);
    TConnection result;
    result.Host = address.first;
    result.Port = address.second;
    result.ConnectionTimeout = TDuration::MilliSeconds(
        Value(directives, (prefix + "ConnectionTimeout"), connectionTimeout.MilliSeconds()));
    result.InteractionTimeout = TDuration::MilliSeconds(
        Value(directives, (prefix + "InteractionTimeout"), interactionTimeout.MilliSeconds()));
    return result;
}

TString TDaemonConfig::THttpOptions::ToString(const TString& sectionName) const {
    TStringStream so;
    so << Endl << "<" << sectionName << ">" << Endl;
    ToStringImpl(so);
    so << "</" << sectionName << ">" << Endl;
    return so.Str();
}

void TDaemonConfig::THttpOptions::ToStringImpl(TStringStream& so) const {
    so << "Port : " << Port << Endl
        << "Host : " << Host << Endl
        << "Threads : " << nThreads << Endl
        << "MaxQueueSize : " << MaxQueueSize << Endl
        << "FThreads : " << nFThreads << Endl
        << "MaxFQueueSize : " << MaxFQueueSize << Endl
        << "MaxConnections : " << MaxConnections << Endl
        << "ClientTimeout : " << ClientTimeout.MilliSeconds() << Endl
        << "KeepAliveEnabled : " << KeepAliveEnabled << Endl
        << "CompressionEnabled : " << CompressionEnabled << Endl
        << "BindAddress : " << BindAddress << Endl
        << "RejectExcessConnections : " << RejectExcessConnections << Endl
        << "ReusePort : " << ReusePort << Endl
        << "ReuseAddress : " << ReuseAddress << Endl
        << "ListenBacklog : " << ListenBacklog << Endl
        << "OutputBufferSize : " << OutputBufferSize << Endl
        << "PollTimeoutMs : " << PollTimeout.MilliSeconds() << Endl
        << "ExpirationTimeoutMs : " << ExpirationTimeout.MilliSeconds() << Endl;
}

void TDaemonConfig::THttpOptions::Init(const TYandexConfig::Directives& directives, bool verifyThreads) {
    *this = TDaemonConfig::ParseHttpServerOptions(directives, TString(), Default<TString>());
    if (BindAddress = directives.Value("BindAddress", Default<TString>())) {
        for (auto&& address : SplitString(BindAddress, " ")) {
            auto parts = SplitString(address, "@", 2, KEEP_EMPTY_TOKENS);
            VERIFY_WITH_LOG(parts.size(), "Incorrect ExtraBindAddress");

            const TString& host = parts[0];
            const ui16 port = (parts.size() > 1) ? FromString<ui16>(parts[1]) : 0;
            AddBindAddress(host, port);
        }
    }
    if (verifyThreads)
        VERIFY_WITH_LOG (nThreads != 0, "'Threads' in THttpOptions cannot be zero");
}

TDaemonConfig::THttpOptions::THttpOptions(const THttpServerOptions& other)
: THttpServerOptions(other)
{}

TDaemonConfig::THttpOptions::THttpOptions()
: THttpServerOptions()
{}

TDaemonConfig::TControllerConfig::TControllerConfig()
    : THttpOptions()
    , StartServer(true)
    , Enabled(true)
    , AutoStop(false)
    , ConfigsControl(false)
    , ReinitLogsOnRereadConfigs(true)
    , EnableNProfile(true)
    , ConfigsRoot(TFsPath(GetExecPath()).Parent().Fix().GetPath() + "/configs")
    , StateRoot(TFsPath(GetExecPath()).Parent().Fix().GetPath() + "/state")
    , VersionsFileName("configs.version")
{
    Port = 24242;
    SetThreadsName("CtrlHttpListen", "CtrlHttpReq", "CtrlHttpFailReq");
}

void TDaemonConfig::TControllerConfig::ToStringImpl(TStringStream& so) const {
    if (Enabled)
        THttpOptions::ToStringImpl(so);
    so << "StartServer : " << StartServer << Endl;
    so << "StateRoot : " << StateRoot << Endl;
    so << "Enabled : " << Enabled << Endl;
    so << "AutoStop : " << AutoStop << Endl;
    so << "ConfigsControl : " << ConfigsControl << Endl;
    so << "ReinitLogsOnRereadConfigs: " << ReinitLogsOnRereadConfigs << Endl;
    so << "EnableNProfile: " << EnableNProfile << Endl;
    so << "ConfigsRoot : " << ConfigsRoot << Endl;
    so << "Log : " << Log << Endl;
    so << "VersionsFileName : " << VersionsFileName << Endl;
    so << DMOptions.ToString() << Endl;
    so << "<Threading>" << Endl;
    Threading.ToString(so);
    so << "</Threading>" << Endl;
}

TString TDaemonConfig::DefaultEmptyConfig = "<DaemonConfig>\n"
"\tLogLevel: 0\n"
"\tLoggerType: null\n"
"</DaemonConfig>";

TString TDaemonConfig::TControllerConfig::GetStatusFileName() const {
    DEBUG_LOG << "Status root is " << StateRoot << Endl;
    return StateRoot + "/controller-state-" + ::ToString(Port);
}

void TDaemonConfig::TControllerConfig::Init(const TYandexConfig::Directives& directives, bool /*verifyThreads*/) {
    GetValue(directives, "Enabled", Enabled);
    if (Enabled)
        THttpOptions::Init(directives);
    GetValue(directives, "StateRoot", StateRoot);
    GetValue(directives, "StartServer", StartServer);
    GetValue(directives, "AutoStop", AutoStop);
    GetValue(directives, "ConfigsControl", ConfigsControl);
    GetValue(directives, "ReinitLogsOnRereadConfigs", ReinitLogsOnRereadConfigs);
    GetValue(directives, "EnableNProfile", EnableNProfile);
    GetValue(directives, "ConfigsRoot", ConfigsRoot);
    GetValue(directives, "Log", Log);
    GetValue(directives, "VersionsFileName", VersionsFileName);
}

void TDaemonConfig::TControllerConfig::Init(const TYandexConfig::Section& section, bool verifyThreads) {
    Init(section.GetDirectives(), verifyThreads);
    TYandexConfig::TSectionsMap sections = section.GetAllChildren();
    TYandexConfig::TSectionsMap::const_iterator iter = sections.find("DMOptions");
    if (iter != sections.end())
        DMOptions.Init(*iter->second);
    iter = sections.find("Threading");
    if (iter != sections.end())
        Threading.Init(iter->second->GetDirectives());
}

void TDaemonConfig::TControllerConfig::TDMOptions::Init(const TYandexConfig::Section& section) {
    const TYandexConfig::Directives& dir = section.GetDirectives();
    GetValue(dir, "Enabled", Enabled);
    if (Enabled)
        *(TConnection*)this = ParseConnection(dir, "");
    GetValue(dir, "UriPrefix", UriPrefix);
    GetValue(dir, "CType", CType);
    GetValue(dir, "ServiceType", ServiceType);
    GetValue(dir, "Service", Service);
    GetValue(dir, "Slot", Slot);
    GetValue(dir, "Attemptions", Attemptions);
    ui32 timeoutS = Timeout.Seconds();
    GetValue(dir, "TimeoutS", timeoutS);
    Timeout = TDuration::Seconds(timeoutS);

}

TString TDaemonConfig::TControllerConfig::TDMOptions::ToString() const {
    TStringStream ss;
    ss << "<DMOptions>" << Endl;
    ss << "Enabled: " << Enabled << Endl;
    ToStringFields(ss);
    ss << "UriPrefix: " << UriPrefix << Endl;
    ss << "CType: " << CType << Endl;
    ss << "ServiceType: " << ServiceType << Endl;
    ss << "Service: " << Service << Endl;
    ss << "Slot: " << Slot << Endl;
    ss << "TimeoutS: " << Timeout.Seconds() << Endl;
    ss << "</DMOptions>" << Endl;
    return ss.Str();
}

NJson::TJsonValue TDaemonConfig::TControllerConfig::TDMOptions::Serialize() const {
    TUnstrictConfig cfg;
    TString str = ToString();
    VERIFY_WITH_LOG(cfg.ParseMemory(str.data()), "invalid  TDaemonConfig::TControllerConfig::TDMOptions::ToString")
    NJson::TJsonValue result;
    cfg.ToJsonPatch(*cfg.GetRootSection()->GetAllChildren().find("DMOptions")->second, result, "");
    return result;
}

void TDaemonConfig::TControllerConfig::TDMOptions::Deserialize(const NJson::TJsonValue& json) {
    NConfigPatcher::TOptions ops;
    ops.Prefix = "DMOptions.";
    TUnstrictConfig cfg;
    TString str = NConfigPatcher::Patch("", json.GetStringRobust(), ops);
    VERIFY_WITH_LOG(cfg.ParseMemory(str.data()), "invalid NConfigPatcher::Patch");
    Init(*cfg.GetRootSection()->GetAllChildren().find("DMOptions")->second);
}

void TDaemonConfig::TConnection::ToStringFields(TStringStream& ss) const {
    ss << "Host: " << Host << Endl;
    ss << "Port: " << Port << Endl;
    ss << "ConnectionTimeout: " << ConnectionTimeout.MilliSeconds() << Endl;
    ss << "InteractionTimeout: " << InteractionTimeout.MilliSeconds() << Endl;
}
