#include <saas/tools/rty_ops/lib/server.h>
#include <saas/tools/rty_ops/lib/util.h>

#include <saas/rtyserver/controller/controller.h>
#include <saas/library/daemon_base/module/module.h>


#include <library/cpp/json/json_reader.h>
#include <library/cpp/json/writer/json_value.h>
#include <library/cpp/yconf/patcher/config_patcher.h>
#include <library/cpp/yconf/patcher/unstrict_config.h>

#include <util/folder/path.h>
#include <util/string/subst.h>


class TRtyOpsOptions: public TDaemonOptions {
public:
    using TDaemonOptions::TDaemonOptions;

    void Parse(int argc, char* argv[]) {
        argc = TVirtualArgs::SplitArgsLikeGdb(argc, argv, VirtualCommandLine);
        TDaemonOptions::Parse(argc, argv);
        //TODO: extend base class, remove the hack
        NLastGetopt::TOpts opts;
        BindToOpts(opts);
        NLastGetopt::TOptsParseResult res(&opts, argc, const_cast<const char**>(argv));

        if (res.GetFreeArgCount() > 1) {
            Mode = res.GetFreeArgs()[res.GetFreeArgCount() - 1];
            size_t nPatchArgs = res.GetFreeArgCount() - 1;
            if (TFsPath(Mode).Exists()) {
                // a fancy alternative way to set the "empty" Mode ;)
                Mode = "";
                nPatchArgs++;
            }

            for (size_t i = 1; i < nPatchArgs; ++i) {
                ConfigPatchesFiles.push_back(res.GetFreeArgs()[i]);
            }
        }

        if (Mode == "Check") { // check and repair - i.e. just start and stop - set empty mode
            Mode = "";
        }
    }

    const TVector<TString>& GetConfigPatchesFiles() const {
        return ConfigPatchesFiles;
    }

    const TString& GetMode() {
        return Mode;
    }

    const TString& GetVirtualArgs() const {
        return VirtualCommandLine;
    }

private:
    TString Mode;
    TString VirtualCommandLine;
    TVector<TString> ConfigPatchesFiles;
};

class TRtyAppContainer {
public:
    using TController = TRTYController;
    using TServer = TRtyOpsApp;
    using TServerDescriptor = TServerDescriptor<TServer>;

private:
    THolder<TController> Server;

private:
    void SetGlobalsInPreprocessorVars(TConfigPatcher& patcher, const TString& mode, const TString& virtualArgs) {
        patcher.SetVariable("RtyOpsMode", mode);
        patcher.SetVariable("RtyOpsArgs", virtualArgs);
    }

    void SetRootDirInPreprocessorVars(TConfigPatcher& patcher, TFsPath rootDir) {
        TVector<TString> vars;
        for (const auto& kv : patcher.GetVariables()) {
            const TString& name = kv.first;
            if (name.find("_PATH") != TString::npos || name.EndsWith("_DIR") || name.EndsWith("_DIRECTORY") || name.EndsWith("_ROOT") || name.EndsWith("Dir")) {
                vars.push_back(name);
            }
        }

        for (const auto& name : vars) {
            const TString value = patcher.GetVariables().at(name);
            const TFsPath path(value);
            if (path.IsDefined() && path.IsRelative()) {
                const TFsPath absPath = rootDir / path;
                INFO_LOG << "Setting root dir: " << name << " = " << absPath.GetPath() << Endl;
                patcher.SetVariable(name, absPath);
            }
        }
    }

    static TMaybe<TString> GetField(const TUnstrictConfig& config, const TString& path) {
        TString vl = config.GetValue(path);
        if (vl == "__not_found__") {
            return TMaybe<TString>();
        }
        return vl;
    }

    static TFsPath GetFieldAsFsPath(const TUnstrictConfig& config, const TString& path) {
        TMaybe<TString> fieldVl = GetField(config, path);
        return TFsPath(fieldVl.GetOrElse(Default<TString>()));
    }


    static void CreateDirForFile(TFsPath path) {
        if (!path.IsDefined() || path.IsRelative())
            return;

        if (!path.Parent().Exists()) {
            path.Parent().MkDirs();
        }
    }

    static void CreateDir(TMaybe<TString> directory) {
        const TFsPath path(directory.GetOrElse(Default<TString>()));
        if (!path.IsDefined() || path.IsRelative())
            return;

        if (path.Exists()) {
            Y_ENSURE(path.IsDirectory() || path.IsSymlink() && path.RealPath().IsDirectory());
        }

        path.MkDirs();
    }

    void CreateWorkspace(const TString& configText) {
        TUnstrictConfig configPreview;
        Y_ENSURE(configPreview.ParseMemory(configText));

        for (const char* key : {
                 "DaemonConfig.StdOut",
                 "DaemonConfig.StdErr",
                 "DaemonConfig.LoggerType",
                 "Server.Searcher.AccessLog",
                 "Server.Searcher.EventLog",
                 "DaemonConfig.Controller.Log",
                 "Indexer.Common.IndexLog"
                 }) {
            const TFsPath logFile = GetFieldAsFsPath(configPreview, key);
            if (!logFile.IsDefined()) {
                continue;
            }
            CreateDirForFile(logFile);
            logFile.DeleteIfExists();
        }

        for (const char* key : {
                 "Server.IndexDir"}) {
            CreateDir(GetField(configPreview, key));
        }

        for (const char* key : {
                "DaemonConfig.Controller.StateRoot"}) {
            const TFsPath stateDir = GetFieldAsFsPath(configPreview, key);
            if (!stateDir.IsDefined()) {
                continue;
            }
            if (stateDir.Exists() && stateDir.IsDirectory()) {
                TVector<TString> files;
                stateDir.ListNames(files);
                for (auto&& name: files) {
                    if (name.StartsWith("controller-state") || name == "last_success")
                        stateDir.Child(name).ForceDelete();
                }
            }
            stateDir.DeleteIfExists();
            CreateDir(stateDir);
        }
    }

    static bool HasLogsRedirect(const TDaemonConfig& daemonConf) {
        return daemonConf.GetStdOut() || daemonConf.GetStdErr();
    }

    static TString ApplySubsts(TStringBuf s, const THashMap<TString, TString>& substs) {
        // no lua and other BS here, just vars

        // BTW: screw the authors of TConfigPreprocessor because a developer should not have to write code like this
        // after passing his job interview
        TStringStream res;
        auto doFlush = [&res, &s](size_t& begin, size_t end) {
            res << s.SubString(begin, end);
            begin = end;
        };
        auto doSkip = [](size_t& begin, size_t newPos) {
            begin = newPos;
        };
        size_t begin = 0;
        const size_t len = s.size();
        for (size_t pos = begin; (pos = s.find("$", pos)) != TString::npos; ) {
            const TStringBuf tail = s.SubString(pos, len - pos);
            if (tail.StartsWith("${")) {
                doFlush(begin, pos);
                const size_t expressionBegin = pos + 2;
                const size_t expressionEnd = s.find('}', expressionBegin);
                Y_ENSURE(expressionEnd != TString::npos); // close your brackets

                TStringBuf key = s.SubString(expressionBegin, expressionEnd - expressionBegin);
                Y_ENSURE(key.find_first_of("${") == TString::npos); // no nesting please

                bool found = substs.contains(key);
                size_t firstSpace = key.find(' ');
                if (!found && firstSpace != TString::npos) {
                    TStringBuf key2 = key.SubString(0, firstSpace);
                    if (!key2.empty() && substs.contains(key2)) {
                        found = true;
                        key = key2;
                    }
                }

                if (found) {
                    // yield value instead of key
                    const TString value = substs.at(key);
                    res << value;
                    pos = expressionEnd + 1;
                    doSkip(begin, pos);
                    continue;
                }

                if (firstSpace != TString::npos) {
                    // a lua expression, yield as is
                    res << "${" << key << "}";
                } else {
                    // yield nothing
                }
                pos = expressionEnd + 1;
                doSkip(begin, pos);
                continue;
            }
            if (tail.StartsWith("$$")) {
                doFlush(begin, pos);
                res << "$";
                pos += 2;
                doSkip(begin, pos);
                continue;
            }
            pos += 1;
        }

        if (begin < len) {
            doFlush(begin, len);
        }

        return res.Str();
    }

    TString ApplyRtyPatch(const TString& originalConfig, const NJson::TJsonValue& jsonPatch, const THashMap<TString, TString>* substs) {
        //TODO: [refactor] adapted from  TController::SetConfigFields - extract a helper method, remove copypaste

        using namespace NJson;
        if (jsonPatch.GetType() == EJsonValueType::JSON_NULL || jsonPatch.GetType() == EJsonValueType::JSON_UNDEFINED) {
            return originalConfig;
        }

        const NJson::TJsonValue::TMapType* patch;
        Y_ENSURE(jsonPatch.GetMapPointer(&patch), "a map is expected");

        TUnstrictConfig serverConfig;
        if (!serverConfig.ParseMemory(originalConfig)) {
            TString errors;
            serverConfig.PrintErrors(errors);
            ythrow yexception() << "errors in server config: " << errors;
        }

        const TString dcPrefix = "DaemonConfig.";
        const TString svPrefix = "Server.";
        const TString zeroPrefix = Default<TString>();
        for (const auto& i : *patch) {
            const TString& path = i.first;
            TString newValue = i.second.GetString();
            if (substs) {
                newValue = ApplySubsts(newValue, *substs);
            }

            // rtyserver_test convention: assume "Server." if it is not "DaemonConfig."
            const TString& prefix = path.StartsWith(dcPrefix) ? zeroPrefix : svPrefix;

            serverConfig.PatchEntry(path, newValue, prefix);
            INFO_LOG << "Applying config patch: " << prefix << path << " = " << newValue << Endl;
        }

        TStringStream result;
        serverConfig.PrintConfig(result);
        return result.Str();
    }

    void ApplyRtyPatches(TString& configText, const TVector<TString>& files, const THashMap<TString, TString>* substs) {
        using namespace NJson;
        TVector<TJsonValue> patches;
        for (const TString& file : files) {
            patches.emplace_back();
            try {
                TFsPath f(file);
                f.CheckExists();

                TFileInput fin(f.GetPath());
                const bool ok = ReadJsonTree(&fin, &patches.back(), /*throwOnError=*/true);
                Y_ENSURE(ok);
            } catch (...) {
                ERROR_LOG << "Error while parsing " << file << Endl;
                throw;
            }

            const auto jsonType = patches.back().GetType();
            Y_ENSURE(jsonType == EJsonValueType::JSON_UNDEFINED ||
                     jsonType == EJsonValueType::JSON_NULL ||
                     jsonType == EJsonValueType::JSON_MAP);
        }

        for (const NJson::TJsonValue& patch : patches) {
            configText = ApplyRtyPatch(configText, patch, substs);
        }
    }

public:
    int Run(int argc, char* argv[]) {
        InitGlobalLog2Console(TLOG_DEBUG);
        try {
            TRtyOpsOptions options;
            options.Parse(argc, argv);

            SetGlobalsInPreprocessorVars(options.GetPreprocessor(), options.GetMode(), options.GetVirtualArgs());
            SetRootDirInPreprocessorVars(options.GetPreprocessor(), TFsPath::Cwd());
            TString configText = options.RunPreprocessor();

            if (!options.GetConfigPatchesFiles().empty()) {
                ApplyRtyPatches(configText, options.GetConfigPatchesFiles(), &options.GetPreprocessor().GetVariables());
            }

            CreateWorkspace(configText);

            TDaemonConfig daemonConfig(configText.data(), false);
            options.GetPreprocessor().SetStrict();

            const bool batchMode = HasLogsRedirect(daemonConfig); // todo: get from opts;
            if (batchMode) {
                INFO_LOG << "Switch logging to file" << Endl;
                daemonConfig.InitLogs();
            }


            INFO_LOG << "App config parsed" << Endl;

            TServerConfigConstructorParams configParams(configText.c_str(), options.GetConfigFileName().c_str(), &options.GetPreprocessor());
            TServerDescriptor sd;
            Server = MakeHolder<TController>(configParams, sd);
            INFO_LOG << "Starting server" << Endl;
            Server->Run();
            Server->WaitStopped();
            Server.Destroy();
            INFO_LOG << "Server stopped" << Endl;

            return EXIT_SUCCESS;
        } catch (yexception& e) {
            FATAL_LOG << "Run failed: " << CurrentExceptionMessage() << Endl;
            return EXIT_FAILURE;
        }
    };
};

int main(int argc, char* argv[]) {
    InitGlobalLog2Console(TLOG_DEBUG);
    return Singleton<TRtyAppContainer>()->Run(argc, argv);
}
