// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest

#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <mutex>
#include <unordered_map>

#include <cstdlib>
#include <errno.h>
#include <fcntl.h>
#include <pwd.h>
#include <signal.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>

#include <boost/exception/diagnostic_information.hpp>
#include <boost/program_options.hpp>

#include <fmt/color.h>
#include <fmt/core.h>
#include <fmt/ranges.h>

#include <everest/logging.hpp>
#include <framework/everest.hpp>
#include <framework/runtime.hpp>
#include <utils/config.hpp>
#include <utils/mqtt_abstraction.hpp>
#include <utils/status_fifo.hpp>

#include "controller/ipc.hpp"
#include "system_unix.hpp"
#include <generated/version_information.hpp>

namespace po = boost::program_options;
namespace fs = std::filesystem;

using namespace Everest;

const auto PARENT_DIED_SIGNAL = SIGTERM;
const int CONTROLLER_IPC_READ_TIMEOUT_MS = 50;
const auto complete_start_time = std::chrono::steady_clock::now();

#ifdef ENABLE_ADMIN_PANEL
class ControllerHandle {
public:
    ControllerHandle(pid_t pid, int socket_fd) : pid(pid), socket_fd(socket_fd) {
        // we do "non-blocking" read
        controller_ipc::set_read_timeout(socket_fd, CONTROLLER_IPC_READ_TIMEOUT_MS);
    }

    void send_message(const nlohmann::json& msg) {
        controller_ipc::send_message(socket_fd, msg);
    }

    controller_ipc::Message receive_message() {
        return controller_ipc::receive_message(socket_fd);
    }

    void shutdown() {
        // FIXME (aw): tbd
    }

    const pid_t pid;

private:
    const int socket_fd;
};
#endif

// Helper struct keeping information on how to start module
struct ModuleStartInfo {
    enum class Language {
        cpp,
        javascript,
        python
    };
    ModuleStartInfo(const std::string& name_, const std::string& printable_name_, Language lang_, const fs::path& path_,
                    std::vector<std::string> capabilities_) :
        name(name_),
        printable_name(printable_name_),
        language(lang_),
        path(path_),
        capabilities(std::move(capabilities_)) {
    }
    std::string name;
    std::string printable_name;
    Language language;
    fs::path path;

    // required capabilities of this module
    std::vector<std::string> capabilities;
};

namespace {
/// \brief Setup common environment variables for everestjs and everestpy
void setup_environment(const ModuleStartInfo& module_info, const RuntimeSettings& rs,
                       const MQTTSettings& mqtt_settings) {
    setenv(EV_MODULE, module_info.name.c_str(), 1);
    setenv(EV_PREFIX, rs.prefix.c_str(), 0);
    setenv(EV_LOG_CONF_FILE, rs.logging_config_file.c_str(), 0);
    setenv(EV_MQTT_EVEREST_PREFIX, mqtt_settings.everest_prefix.c_str(), 0);
    setenv(EV_MQTT_EXTERNAL_PREFIX, mqtt_settings.external_prefix.c_str(), 0);
    if (mqtt_settings.uses_socket()) {
        setenv(EV_MQTT_BROKER_SOCKET_PATH, mqtt_settings.broker_socket_path.c_str(), 0);
    } else {
        setenv(EV_MQTT_BROKER_HOST, mqtt_settings.broker_host.c_str(), 0);
        setenv(EV_MQTT_BROKER_PORT, std::to_string(mqtt_settings.broker_port).c_str(), 0);
    }

    if (rs.validate_schema) {
        setenv(EV_VALIDATE_SCHEMA, "1", 1);
    }
}

static void exec_module(const std::string& bin, std::vector<std::string>& arguments, system::SubProcess& proc_handle) {
    // Convert the argument list to the format required by `execv*()`.
    std::vector<char*> argv_list(arguments.size() + 1);
    std::transform(arguments.begin(), arguments.end(), argv_list.begin(), [](auto& value) { return value.data(); });
    argv_list.back() = nullptr; // Add a null terminator

    // Execute the module binary, replacing the current process.
    execvp(bin.c_str(), argv_list.data());

    // `execv()` failed, notify the parent process and exit.
    const auto msg = fmt::format("Syscall to execv() with \"{} {}\" failed ({})", bin,
                                 fmt::join(arguments.begin() + 1, arguments.end(), " "), strerror(errno));
    proc_handle.send_error_and_exit(msg);
}

void exec_cpp_module(system::SubProcess& proc_handle, const ModuleStartInfo& module_info, const RuntimeSettings& rs,
                     const MQTTSettings& mqtt_settings) {
    std::vector<std::string> arguments = {
        module_info.printable_name,
        "--prefix",
        rs.prefix.string(),
        "--module",
        module_info.name,
        "--log_config",
        rs.logging_config_file.string(),
        "--mqtt_everest_prefix",
        mqtt_settings.everest_prefix,
        "--mqtt_external_prefix",
        mqtt_settings.external_prefix}; // TODO: check if this is empty and do not append if needed?

    if (mqtt_settings.uses_socket()) {
        arguments.insert(arguments.end(), {"--mqtt_broker_socket_path", mqtt_settings.broker_socket_path});
    } else {
        arguments.insert(arguments.end(), {"--mqtt_broker_host", mqtt_settings.broker_host, "--mqtt_broker_port",
                                           std::to_string(mqtt_settings.broker_port)});
    }

    exec_module(module_info.path.string(), arguments, proc_handle);
}

void exec_javascript_module(system::SubProcess& proc_handle, const ModuleStartInfo& module_info,
                            const RuntimeSettings& rs, const MQTTSettings& mqtt_settings) {
    // instead of using setenv, using execvpe might be a better way for a controlled environment!

    // FIXME (aw): everest directory layout
    const auto node_modules_path = rs.prefix / defaults::LIB_DIR / defaults::NAMESPACE / "node_modules";
    setenv("NODE_PATH", node_modules_path.c_str(), 0);
    setup_environment(module_info, rs, mqtt_settings);

    std::vector<std::string> arguments = {
        "node",
        "--unhandled-rejections=strict",
        module_info.path.string(),
    };

    exec_module("node", arguments, proc_handle);
}

void exec_python_module(system::SubProcess& proc_handle, const ModuleStartInfo& module_info, const RuntimeSettings& rs,
                        const MQTTSettings& mqtt_settings) {
    // instead of using setenv, using execvpe might be a better way for a controlled environment!

    setup_environment(module_info, rs, mqtt_settings);

    // Prepend the everestpy path to $PYTHONPATH. This ensures modules can always find everestpy.
    const auto everestpy_path = rs.prefix / defaults::LIB_DIR / defaults::NAMESPACE / "everestpy";
    if (const auto prev_pythonpath = std::getenv("PYTHONPATH")) {
        const auto pythonpath = fmt::format("{}:{}", everestpy_path.string(), prev_pythonpath);
        setenv("PYTHONPATH", pythonpath.c_str(), 1);
    } else {
        setenv("PYTHONPATH", everestpy_path.c_str(), 1);
    }

    std::string python_binary = "python3";

    // Check if a virtual environment exists in the module directory, and if so use its python runtime.
    const auto venv_dir = module_info.path.parent_path() / ".venv";
    if (fs::exists(venv_dir)) {
        const auto venv_bin_dir = venv_dir / "bin";
        const auto venv_python = venv_bin_dir / "python3";
        if (fs::exists(venv_python)) {
            // Activate the virtual environment. This approximates the behaviour of the `.venv/bin/activate` script.
            python_binary = venv_python.string();
            setenv("VIRTUAL_ENV", venv_dir.c_str(), 1);
            setenv("VIRTUAL_ENV_PROMPT", "venv", 1);
            unsetenv("PYTHONHOME");

            if (const auto prev_path = std::getenv("PATH")) {
                const auto path = fmt::format("{}:{}", venv_bin_dir.string(), prev_path);
                setenv("PATH", path.c_str(), 1);
            } else {
                setenv("PATH", venv_bin_dir.c_str(), 1);
            }
        }
    }

    std::vector<std::string> arguments = {python_binary, module_info.path.c_str()};
    exec_module(python_binary, arguments, proc_handle);
}

void exec_module(const RuntimeSettings& rs, const MQTTSettings& mqtt_settings, const ModuleStartInfo& module,
                 system::SubProcess& proc_handle) {
    switch (module.language) {
    case ModuleStartInfo::Language::cpp:
        exec_cpp_module(proc_handle, module, rs, mqtt_settings);
        break;
    case ModuleStartInfo::Language::javascript:
        exec_javascript_module(proc_handle, module, rs, mqtt_settings);
        break;
    case ModuleStartInfo::Language::python:
        exec_python_module(proc_handle, module, rs, mqtt_settings);
        break;
    default:
        throw std::logic_error("Module language not in enum");
        break;
    }
}

std::map<pid_t, std::string> spawn_modules(const std::vector<ModuleStartInfo>& modules, const ManagerSettings& ms) {
    std::map<pid_t, std::string> started_modules;

    const auto& rs = ms.runtime_settings;

    for (const auto& module : modules) {

        auto proc_handle = system::SubProcess::create(ms.run_as_user, module.capabilities);

        if (proc_handle.is_child()) {
            // first, check if we need any capabilities

            try {
                exec_module(rs, ms.mqtt_settings, module, proc_handle);
            } catch (const std::exception& err) {
                proc_handle.send_error_and_exit(err.what());
            }
        }

        // we can only come here, if we're the parent!
        const auto child_pid = proc_handle.check_child_executed();

        EVLOG_debug << fmt::format("Forked module {} with pid: {}", module.name, child_pid);
        started_modules[child_pid] = module.name;
    }

    return started_modules;
}
} // namespace

struct ModuleReadyInfo {
    bool ready;
    std::shared_ptr<TypedHandler> ready_token;
    std::shared_ptr<TypedHandler> get_config_token;
};

// FIXME (aw): these are globals here, because they are used in the ready callback handlers
using ModulesReadyType = std::unordered_map<std::string, ModuleReadyInfo>;
namespace {
ModulesReadyType modules_ready; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Don't hold the mutex and use any function of the `mqtt_abstraction` since the
// mutex is also held inside the `ready` handler which can deadlock.
std::mutex modules_ready_mutex; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

std::map<pid_t, std::string> start_modules(ManagerConfig& config, MQTTAbstraction& mqtt_abstraction,
                                           const std::vector<std::string>& ignored_modules,
                                           const std::vector<std::string>& standalone_modules,
                                           const ManagerSettings& ms, StatusFifo& status_fifo, bool retain_topics) {
    BOOST_LOG_FUNCTION();

    std::vector<ModuleStartInfo> modules_to_spawn;

    const auto& module_configurations = config.get_module_configurations();
    const auto& module_names = config.get_module_names();
    modules_to_spawn.reserve(module_configurations.size());
    const auto number_of_modules = module_configurations.size();
    EVLOG_info << "Starting " << number_of_modules << " modules";

    // TODO: move this into its own functions / class? ConfigService?
    const auto interface_definitions = config.get_interface_definitions();
    std::vector<std::string> interface_names;
    for (auto& interface_definition : interface_definitions.items()) {
        interface_names.push_back(interface_definition.key());
    }

    MqttMessagePayload payload{MqttMessageType::GetConfigResponse, interface_names};

    mqtt_abstraction.publish(fmt::format("{}interfaces", ms.mqtt_settings.everest_prefix), payload, QOS::QOS2, true);

    for (const auto& interface_definition : interface_definitions.items()) {

        MqttMessagePayload interface_definition_payload{MqttMessageType::GetConfigResponse,
                                                        interface_definition.value()};

        mqtt_abstraction.publish(
            fmt::format("{}interface_definitions/{}", ms.mqtt_settings.everest_prefix, interface_definition.key()),
            interface_definition_payload, QOS::QOS2, true);
    }

    const auto type_definitions = config.get_types();
    std::vector<std::string> type_names;
    for (auto& type_definition : type_definitions.items()) {
        type_names.push_back(type_definition.key());
    }

    MqttMessagePayload type_names_payload{MqttMessageType::GetConfigResponse, type_names};

    mqtt_abstraction.publish(fmt::format("{}types", ms.mqtt_settings.everest_prefix), type_names_payload, QOS::QOS2,
                             true);
    for (const auto& type_definition : type_definitions.items()) {

        MqttMessagePayload type_definition_payload{MqttMessageType::GetConfigResponse, type_definition.value()};

        // type_definition keys already start with a / so omit it in the topic name
        mqtt_abstraction.publish(
            fmt::format("{}type_definitions{}", ms.mqtt_settings.everest_prefix, type_definition.key()),
            type_definition_payload, QOS::QOS2, true);
    }

    const auto settings = config.get_settings();

    MqttMessagePayload settings_payload{MqttMessageType::GetConfigResponse, settings};

    mqtt_abstraction.publish(fmt::format("{}settings", ms.mqtt_settings.everest_prefix), settings_payload, QOS::QOS2,
                             true);

    if (ms.runtime_settings.validate_schema) {
        const auto schemas = config.get_schemas();

        MqttMessagePayload schemas_payload{MqttMessageType::GetConfigResponse, schemas};

        mqtt_abstraction.publish(fmt::format("{}schemas", ms.mqtt_settings.everest_prefix), schemas_payload, QOS::QOS2,
                                 true);
    }
    const auto manifests = config.get_manifests();

    for (const auto& manifest : manifests.items()) {
        auto manifest_copy = manifest.value();
        manifest_copy.erase("config");

        MqttMessagePayload manifest_payload{MqttMessageType::GetConfigResponse, manifest_copy};

        mqtt_abstraction.publish(fmt::format("{}manifests/{}", ms.mqtt_settings.everest_prefix, manifest.key()),
                                 manifest_payload, QOS::QOS2, true);
    }

    MqttMessagePayload module_names_payload{MqttMessageType::GetConfigResponse, module_names};

    mqtt_abstraction.publish(fmt::format("{}module_names", ms.mqtt_settings.everest_prefix), module_names_payload,
                             QOS::QOS2, true);

    for (const auto& [module_id_, module_config] : module_configurations) {
        const auto& module_name = module_config.module_name;
        const auto& module_id = module_id_;
        if (std::any_of(ignored_modules.begin(), ignored_modules.end(),
                        [module_id](const auto& element) { return element == module_id; })) {
            EVLOG_info << fmt::format("Ignoring module: {}", module_id);
            continue;
        }

        // FIXME (aw): implicitly adding ModuleReadyInfo and setting its ready member
        auto module_it = modules_ready.emplace(module_id, ModuleReadyInfo{false, nullptr, nullptr}).first;

        std::vector<std::string> capabilities =
            module_configurations.at(module_id).capabilities.value_or(std::vector<std::string>{});

        if (not capabilities.empty()) {
            EVLOG_info << fmt::format("Module {} wants to acquire the following capabilities: {}", module_name,
                                      fmt::join(capabilities.begin(), capabilities.end(), " "));
        }

        const Handler module_ready_handler = [module_id, &mqtt_abstraction, &config, standalone_modules,
                                              mqtt_everest_prefix = ms.mqtt_settings.everest_prefix, &status_fifo,
                                              retain_topics](const std::string&, const nlohmann::json& json) {
            EVLOG_debug << fmt::format("received module ready signal for module: {}({})", module_id, json.dump());
            const std::unique_lock<std::mutex> lock(modules_ready_mutex);
            // FIXME (aw): here are race conditions, if the ready handler gets called while modules are shut down!
            try {
                modules_ready.at(module_id).ready = json.get<bool>();
            } catch (const std::out_of_range& ex) {
                // This can happen if we're shutting down and a module becomes
                // ready.
                EVLOG_error << "The module " << module_id << " is not in `modules_ready`: " << ex.what();
                return;
            }
            std::size_t modules_spawned = 0;
            for (const auto& mod : modules_ready) {
                const std::string text_ready =
                    fmt::format((mod.second.ready) ? TERMINAL_STYLE_OK : TERMINAL_STYLE_ERROR, "ready");
                EVLOG_debug << fmt::format("  {}: {}", mod.first, text_ready);
                if (mod.second.ready) {
                    modules_spawned += 1;
                }
            }
            if (!standalone_modules.empty() && std::find(standalone_modules.begin(), standalone_modules.end(),
                                                         module_id) != standalone_modules.end()) {
                EVLOG_info << fmt::format("Standalone module {} initialized.", module_id);
            }
            if (std::all_of(modules_ready.begin(), modules_ready.end(),
                            [](const auto& element) { return element.second.ready; })) {
                const auto complete_end_time = std::chrono::steady_clock::now();
                status_fifo.update(StatusFifo::ALL_MODULES_STARTED);
                if (not retain_topics) {
                    EVLOG_info << "Clearing retained topics published by manager during startup";
                    mqtt_abstraction.clear_retained_topics();
                } else {
                    EVLOG_info << "Keeping retained topics published by manager during startup for inspection";
                }
                EVLOG_info << fmt::format(
                    TERMINAL_STYLE_OK, "🚙🚙🚙 All modules are initialized. EVerest up and running [{}ms] 🚙🚙🚙",
                    std::chrono::duration_cast<std::chrono::milliseconds>(complete_end_time - complete_start_time)
                        .count());

                MqttMessagePayload payload{MqttMessageType::GlobalReady, nlohmann::json(true)};

                mqtt_abstraction.publish(fmt::format("{}ready", mqtt_everest_prefix), payload);
            } else if (!standalone_modules.empty()) {
                if (modules_spawned == modules_ready.size() - standalone_modules.size()) {
                    EVLOG_info << fmt::format(fg(fmt::terminal_color::green),
                                              "Modules started by manager are ready, waiting for standalone modules.");
                    status_fifo.update(StatusFifo::WAITING_FOR_STANDALONE_MODULES);
                }
            }
        };

        const std::string ready_topic = fmt::format("{}/ready", config.mqtt_module_prefix(module_id));
        module_it->second.ready_token =
            std::make_shared<TypedHandler>(HandlerType::ModuleReady, std::make_shared<Handler>(module_ready_handler));
        mqtt_abstraction.register_handler(ready_topic, module_it->second.ready_token, QOS::QOS2);

        if (std::any_of(standalone_modules.begin(), standalone_modules.end(),
                        [module_id](const auto& element) { return element == module_id; })) {
            EVLOG_info << "Not starting standalone module: " << fmt::format(TERMINAL_STYLE_BLUE, "{}", module_id);
            continue;
        }

        const std::string binary_filename = fmt::format("{}", module_name);
        const std::string javascript_library_filename = "index.js";
        const std::string python_filename = "module.py";
        const auto module_path = ms.runtime_settings.modules_dir / module_name;
        const auto printable_module_name = config.printable_identifier(module_id);
        const auto binary_path = module_path / binary_filename;
        const auto javascript_library_path = module_path / javascript_library_filename;
        const auto python_module_path = module_path / python_filename;

        if (fs::exists(binary_path)) {
            EVLOG_debug << fmt::format("module: {} ({}) provided as binary", module_id, module_name);
            modules_to_spawn.emplace_back(module_id, printable_module_name, ModuleStartInfo::Language::cpp, binary_path,
                                          capabilities);
        } else if (fs::exists(javascript_library_path)) {
            EVLOG_debug << fmt::format("module: {} ({}) provided as javascript library", module_id, module_name);
            modules_to_spawn.emplace_back(module_id, printable_module_name, ModuleStartInfo::Language::javascript,
                                          fs::canonical(javascript_library_path), capabilities);
        } else if (fs::exists(python_module_path)) {
            EVLOG_verbose << fmt::format("module: {} ({}) provided as python module", module_id, module_name);
            modules_to_spawn.emplace_back(module_id, printable_module_name, ModuleStartInfo::Language::python,
                                          fs::canonical(python_module_path), capabilities);
        } else {
            if (module_id == "probe" || module_name == "ProbeModule") {
                EVLOG_error << "You are trying to start the probe module as binary, please check "
                               "your test case, did you add \"@pytest.mark.probe_module\" to your test case?";
            }
            throw std::runtime_error(
                fmt::format("module: {} ({}) cannot be loaded because no Binary, JavaScript or Python "
                            "module has been found\n"
                            "  checked paths:\n"
                            "    binary: {}\n"
                            "    js:  {}\n"
                            "    py:  {}\n",
                            module_id, module_name, binary_path.string(), javascript_library_path.string(),
                            python_module_path.string()));
        }
    }

    return spawn_modules(modules_to_spawn, ms);
}

void shutdown_modules(const std::map<pid_t, std::string>& modules, ManagerConfig& config,
                      MQTTAbstraction& mqtt_abstraction) {

    ModulesReadyType modules_ready_moved;
    {
        const std::lock_guard<std::mutex> lck(modules_ready_mutex);
        modules_ready_moved = std::move(modules_ready);
        // Probably not needed after our move but lets be explicit.
        modules_ready.clear();
    }

    for (const auto& module : modules_ready_moved) {
        const auto& ready_info = module.second;
        const auto& module_name = module.first;
        const std::string topic = fmt::format("{}/ready", config.mqtt_module_prefix(module_name));
        mqtt_abstraction.unregister_handler(topic, ready_info.ready_token);
    }

    for (const auto& child : modules) {
        auto retval = kill(child.first, SIGTERM);
        // FIXME (aw): supply errno strings
        if (retval != 0) {
            EVLOG_critical << fmt::format("SIGTERM of child: {} (pid: {}) {}: {}. Escalating to SIGKILL", child.second,
                                          child.first, fmt::format(TERMINAL_STYLE_ERROR, "failed"), retval);
            retval = kill(child.first, SIGKILL);
            if (retval != 0) {
                EVLOG_critical << fmt::format("SIGKILL of child: {} (pid: {}) {}: {}.", child.second, child.first,
                                              fmt::format(TERMINAL_STYLE_ERROR, "failed"), retval);
            } else {
                EVLOG_info << fmt::format("SIGKILL of child: {} (pid: {}) {}.", child.second, child.first,
                                          fmt::format(TERMINAL_STYLE_OK, "succeeded"));
            }
        } else {
            EVLOG_info << fmt::format("SIGTERM of child: {} (pid: {}) {}.", child.second, child.first,
                                      fmt::format(TERMINAL_STYLE_OK, "succeeded"));
        }
    }
}

#ifdef ENABLE_ADMIN_PANEL
ControllerHandle start_controller(const ManagerSettings& ms) {
    std::array<int, 2> socket_pair; // NOLINT(cppcoreguidelines-pro-type-member-init): this is always initialized in the
                                    // following socketpair call

    // FIXME (aw): destroy this socketpair somewhere
    socketpair(AF_UNIX, SOCK_DGRAM, 0, socket_pair.data());
    const int manager_socket = socket_pair[0];
    const int controller_socket = socket_pair[1];

    auto proc_handle = system::SubProcess::create(ms.run_as_user);

    if (proc_handle.is_child()) {
        // FIXME (aw): hack to get the correct directory of the controller
        const auto bin_dir = fs::canonical("/proc/self/exe").parent_path();

        const auto controller_binary = bin_dir / "controller";

        close(manager_socket);
        dup2(controller_socket, STDIN_FILENO);
        close(controller_socket);

        execl(controller_binary.c_str(), MAGIC_CONTROLLER_ARG0, NULL);

        // exec failed
        proc_handle.send_error_and_exit(
            fmt::format("Syscall to execl() with \"{} {}\" failed ({})", controller_binary.string(), strerror(errno)));
    }

    close(controller_socket);

    // send initial config to controller
    controller_ipc::send_message(manager_socket,
                                 {
                                     {"method", "boot"},
                                     {"params",
                                      {
                                          {"module_dir", ms.runtime_settings.modules_dir.string()},
                                          {"interface_dir", ms.interfaces_dir.string()},
                                          {"www_dir", ms.www_dir.string()},
                                          {"configs_dir", ms.configs_dir.string()},
                                          {"logging_config_file", ms.runtime_settings.logging_config_file.string()},
                                          {"controller_port", ms.controller_port},
                                          {"controller_rpc_timeout_ms", ms.controller_rpc_timeout_ms},
                                      }},
                                 });

    return {proc_handle.check_child_executed(), manager_socket};
}
#endif

ConfigBootMode parse_config_boot_mode(const std::string& config_opt, const std::string& db_opt, const bool db_init) {
    if (config_opt.empty() and db_opt.empty()) {
        // no config or db option given, use default
        return ConfigBootMode::YamlFile;
    }
    if (!config_opt.empty() && !db_opt.empty()) {
        if (db_init == false) {
            throw BootException("Both --config and --db options are set, but no --db-init option is given. "
                                "This is not allowed.");
        }
        return ConfigBootMode::DatabaseInit;
    }
    if (!config_opt.empty()) {
        // only config option given, use yaml file
        return ConfigBootMode::YamlFile;
    }
    if (!db_opt.empty()) {
        // only db option given, use database
        return ConfigBootMode::Database;
    }
    throw std::logic_error("Could not parse config boot source, this should never happen.");
}

int boot(const po::variables_map& vm) {
    const bool check = (vm.count("check") != 0);

    const auto prefix_opt = parse_string_option(vm, "prefix");
    const auto config_opt = parse_string_option(vm, "config");
    const auto db_opt = parse_string_option(vm, "db");
    const auto db_init = vm.count("db-init") != 0;
    ConfigBootMode boot_mode = parse_config_boot_mode(config_opt, db_opt, db_init);

    ManagerSettings ms;

    switch (boot_mode) {
    case ConfigBootMode::YamlFile:
        ms = ManagerSettings(prefix_opt, config_opt);
        break;
    case ConfigBootMode::Database:
        ms = ManagerSettings(prefix_opt, db_opt, DatabaseTag{});
        break;
    case ConfigBootMode::DatabaseInit:
        ms = ManagerSettings(prefix_opt, config_opt, db_opt);
        break;
    default:
        throw BootException(fmt::format("Invalid boot source: {}", static_cast<int>(boot_mode)));
    }

    // CLI override for mqtt_everest_prefix (e.g. for parallel test execution).
    if (vm.count("mqtt_everest_prefix") != 0) {
        auto prefix = vm["mqtt_everest_prefix"].as<std::string>();
        if (!prefix.empty() && prefix.back() != '/') {
            prefix += "/";
        }
        ms.mqtt_settings.everest_prefix = prefix;
    }

    Logging::init(ms.runtime_settings.logging_config_file.string());

    EVLOG_info << "  \033[0;1;35;95m_\033[0;1;31;91m__\033[0;1;33;93m__\033[0;1;32;92m__\033[0;1;36;96m_\033[0m      "
                  "\033[0;1;31;91m_\033[0;1;33;93m_\033[0m                \033[0;1;36;96m_\033[0m   ";
    EVLOG_info << " \033[0;1;31;91m|\033[0m  \033[0;1;33;93m_\033[0;1;32;92m__\033[0;1;36;96m_\\\033[0m "
                  "\033[0;1;34;94m\\\033[0m    \033[0;1;33;93m/\033[0m \033[0;1;32;92m/\033[0m               "
                  "\033[0;1;34;94m|\033[0m \033[0;1;35;95m|\033[0m";
    EVLOG_info
        << " \033[0;1;33;93m|\033[0m \033[0;1;32;92m|_\033[0;1;36;96m_\033[0m   \033[0;1;35;95m\\\033[0m "
           "\033[0;1;31;91m\\\033[0m  \033[0;1;33;93m/\033[0m \033[0;1;32;92m/\033[0;1;36;96m__\033[0m "
           "\033[0;1;34;94m_\033[0m \033[0;1;35;95m_\033[0;1;31;91m_\033[0m \033[0;1;33;93m__\033[0;1;32;92m_\033[0m  "
           "\033[0;1;36;96m_\033[0;1;34;94m__\033[0;1;35;95m|\033[0m \033[0;1;31;91m|_\033[0m";
    EVLOG_info << " \033[0;1;32;92m|\033[0m  \033[0;1;36;96m_\033[0;1;34;94m_|\033[0m   \033[0;1;31;91m\\\033[0m "
                  "\033[0;1;33;93m\\\033[0;1;32;92m/\033[0m \033[0;1;36;96m/\033[0m \033[0;1;34;94m_\033[0m "
                  "\033[0;1;35;95m\\\033[0m \033[0;1;31;91m'_\033[0;1;33;93m_/\033[0m \033[0;1;32;92m_\033[0m "
                  "\033[0;1;36;96m\\\033[0;1;34;94m/\033[0m \033[0;1;35;95m__\033[0;1;31;91m|\033[0m "
                  "\033[0;1;33;93m__\033[0;1;32;92m|\033[0m";
    EVLOG_info << " \033[0;1;36;96m|\033[0m \033[0;1;34;94m|_\033[0;1;35;95m__\033[0;1;31;91m_\033[0m   "
                  "\033[0;1;32;92m\\\033[0m  \033[0;1;36;96m/\033[0m  \033[0;1;35;95m__\033[0;1;31;91m/\033[0m "
                  "\033[0;1;33;93m|\033[0m \033[0;1;32;92m|\033[0m  "
                  "\033[0;1;36;96m_\033[0;1;34;94m_/\033[0;1;35;95m\\_\033[0;1;31;91m_\033[0m \033[0;1;33;93m\\\033[0m "
                  "\033[0;1;32;92m|_\033[0m";
    EVLOG_info << " \033[0;1;34;94m|_\033[0;1;35;95m__\033[0;1;31;91m__\033[0;1;33;93m_|\033[0m   "
                  "\033[0;1;36;96m\\\033[0;1;34;94m/\033[0m "
                  "\033[0;1;35;95m\\_\033[0;1;31;91m__\033[0;1;33;93m|_\033[0;1;32;92m|\033[0m  "
                  "\033[0;1;36;96m\\\033[0;1;34;94m__\033[0;1;35;95m_|\033[0;1;31;91m|_\033[0;1;33;93m__\033[0;1;32;"
                  "92m/\\\033[0;1;36;96m__\033[0;1;34;94m|\033[0m";
    EVLOG_info << "";
    EVLOG_info << PROJECT_NAME << " " << PROJECT_VERSION << " " << GIT_VERSION;
    EVLOG_info << ms.version_information;
    EVLOG_info << "";

    if (not ms.mqtt_settings.uses_socket()) {
        EVLOG_info << "Using MQTT broker " << ms.mqtt_settings.broker_host << ":" << ms.mqtt_settings.broker_port;
    } else {
        EVLOG_info << "Using MQTT broker unix domain sockets:" << ms.mqtt_settings.broker_socket_path;
    }
    if (ms.runtime_settings.telemetry_enabled) {
        EVLOG_info << "Telemetry enabled";
    }
    if (not ms.run_as_user.empty()) {
        EVLOG_info << "EVerest will run as system user: " << ms.run_as_user;
    }
    if (ms.runtime_settings.forward_exceptions) {
        EVLOG_info << "Catching and forwarding command exceptions to callers";
    }

#ifdef ENABLE_ADMIN_PANEL
    auto controller_handle = start_controller(ms);
#endif

    EVLOG_verbose << fmt::format("EVerest prefix was set to {}", ms.runtime_settings.prefix.string());

    // dump all manifests if requested and terminate afterwards
    if (vm.count("dumpmanifests")) {
        const auto dumpmanifests_path = fs::path(vm["dumpmanifests"].as<std::string>());
        EVLOG_debug << fmt::format("Dumping all known validated manifests into '{}'", dumpmanifests_path.string());

        auto manifests = Config::load_all_manifests(ms.runtime_settings.modules_dir.string(), ms.schemas_dir.string());

        for (const auto& module : manifests.items()) {
            const std::string filename = module.key() + ".yaml";
            const auto module_output_path = dumpmanifests_path / filename;
            // FIXME (aw): should we check if the directory exists?
            std::ofstream output_stream(module_output_path);

            // FIXME (aw): this should be either YAML prettyfied, or better, directly copied
            output_stream << module.value().dump(DUMP_INDENT);
        }

        return EXIT_SUCCESS;
    }

    const bool retain_topics = (vm.count("retain-topics") != 0);

    const auto start_time = std::chrono::steady_clock::now();
    std::shared_ptr<ManagerConfig> config; // TODO: maybe this can stay unique when we re-work start_modules()
    try {
        config = std::make_shared<ManagerConfig>(ms);
    } catch (EverestInternalError& e) {
        EVLOG_error << fmt::format("Failed to load and validate config!\n{}", boost::diagnostic_information(e, true));
        return EXIT_FAILURE;
    } catch (boost::exception& e) {
        EVLOG_error << "Failed to load and validate config!";
        EVLOG_critical << fmt::format("Caught top level boost::exception:\n{}", boost::diagnostic_information(e, true));
        return EXIT_FAILURE;
    } catch (std::exception& e) {
        EVLOG_error << "Failed to load and validate config!";
        EVLOG_critical << fmt::format("Caught top level std::exception:\n{}", boost::diagnostic_information(e, true));
        return EXIT_FAILURE;
    }
    const auto end_time = std::chrono::steady_clock::now();
    EVLOG_info << "Config loading completed in "
               << std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count() << "ms";

    // dump config if requested
    if (vm.count("dump")) {
        const auto dump_path = fs::path(vm["dump"].as<std::string>());
        EVLOG_debug << fmt::format("Dumping validated config and manifests into '{}'", dump_path.string());

        const auto config_dump_path = dump_path / "config.json";

        std::ofstream output_config_stream(config_dump_path);

        output_config_stream << json(config->get_module_configurations()).dump(DUMP_INDENT);

        const auto manifests = config->get_manifests();

        for (const auto& module : manifests.items()) {
            const std::string filename = module.key() + ".json";
            const auto module_output_path = dump_path / filename;
            std::ofstream output_stream(module_output_path);

            output_stream << module.value().dump(DUMP_INDENT);
        }
    }

    // only config check (and or config dumping) was requested, log check result and exit
    if (check) {
        EVLOG_debug << "Config is valid, terminating as requested";
        return EXIT_SUCCESS;
    }

    std::vector<std::string> standalone_modules;
    if (vm.count("standalone")) {
        standalone_modules = vm["standalone"].as<std::vector<std::string>>();
    }

    const auto& module_configurations = config->get_module_configurations();
    for (const auto& [module_id, module_config] : module_configurations) {
        // check if standalone parameter is set
        if (module_config.standalone) {
            if (std::find(standalone_modules.begin(), standalone_modules.end(), module_id) ==
                standalone_modules.end()) {
                EVLOG_info << "Module " << fmt::format(TERMINAL_STYLE_BLUE, "{}", module_id)
                           << " marked as standalone in config";

                standalone_modules.push_back(module_id);
            }
        }
    }

    std::vector<std::string> ignored_modules;
    if (vm.count("ignore")) {
        ignored_modules = vm["ignore"].as<std::vector<std::string>>();
    }

    // create StatusFifo object
    auto status_fifo = StatusFifo::create_from_path(vm["status-fifo"].as<std::string>());

    auto mqtt_abstraction = make_mqtt_abstraction(ms.mqtt_settings);

    if (!mqtt_abstraction->connect()) {
        if (not ms.mqtt_settings.uses_socket()) {
            EVLOG_error << fmt::format("Cannot connect to MQTT broker at {}:{}", ms.mqtt_settings.broker_host,
                                       ms.mqtt_settings.broker_port);
        } else {
            EVLOG_error << fmt::format("Cannot connect to MQTT broker socket at {}",
                                       ms.mqtt_settings.broker_socket_path);
        }
        return EXIT_FAILURE;
    }

    mqtt_abstraction->spawn_main_loop_thread();

    auto config_service = std::make_unique<config::ConfigService>(*mqtt_abstraction, config);

    auto module_handles =
        start_modules(*config, *mqtt_abstraction, ignored_modules, standalone_modules, ms, status_fifo, retain_topics);
    bool modules_started = true;
    bool restart_modules = false;

#ifndef ENABLE_ADMIN_PANEL
    // switch to low privilege user if configured
    if (not ms.run_as_user.empty()) {
        auto err_set_user = system::set_real_user(ms.run_as_user);
        if (not err_set_user.empty()) {
            EVLOG_error << "Error switching manager to user " << ms.run_as_user << ": " << err_set_user;
            return EXIT_FAILURE;
        }
    }
#endif

    int wstatus; // NOLINT(cppcoreguidelines-init-variables): this is always initialized in the following waitpid call

    while (true) {
// check if anyone died
#ifdef ENABLE_ADMIN_PANEL
        // non-blocking if admin panel is enabled, as this main loop also processes controller RPC
        auto pid = waitpid(-1, &wstatus, WNOHANG);
#else
        // block if admin panel is disabled, no controller RPC is handled by main loop
        auto pid = waitpid(-1, &wstatus, 0);
#endif

        if (pid == 0) {
            // nothing new from our child process
        } else if (pid == -1) {
            throw std::runtime_error(fmt::format("Syscall to waitpid() failed ({})", strerror(errno)));
        } else {

#ifdef ENABLE_ADMIN_PANEL
            // one of our children exited (first check controller, then modules)
            if (pid == controller_handle.pid) {
                // FIXME (aw): what to do, if the controller exited? Restart it?
                throw std::runtime_error("The controller process exited.");
            }
#endif

            const auto module_iter = module_handles.find(pid);
            if (module_iter == module_handles.end()) {
                throw std::runtime_error(fmt::format("Unknown child width pid ({}) died.", pid));
            }

            const auto module_name = module_iter->second;
            module_handles.erase(module_iter);
            // one of our modules died -> kill 'em all
            if (modules_started) {
                EVLOG_critical << fmt::format("Module {} (pid: {}) exited with status: {}. Terminating all modules.",
                                              module_name, pid, wstatus);
                shutdown_modules(module_handles, *config, *mqtt_abstraction);

                mqtt_abstraction->clear_retained_topics();
                mqtt_abstraction->disconnect();

                // Exit if a module died, this gives systemd a change to restart manager
                EVLOG_critical << "Exiting manager.";
                return EXIT_FAILURE;
            } else {
                EVLOG_info << fmt::format("Module {} (pid: {}) exited with status: {}.", module_name, pid, wstatus);
            }
        }

#ifdef ENABLE_ADMIN_PANEL
        if (module_handles.size() == 0 && restart_modules) {
            module_handles = start_modules(*config, *mqtt_abstraction, ignored_modules, standalone_modules, ms,
                                           status_fifo, retain_topics);
            restart_modules = false;
            modules_started = true;
        }

        // check for news from the controller
        const auto msg = controller_handle.receive_message();
        if (msg.status == controller_ipc::MESSAGE_RETURN_STATUS::OK) {
            // FIXME (aw): implement all possible messages here, for now just log them
            const auto& payload = msg.json;
            if (payload.at("method") == "restart_modules") {
                shutdown_modules(module_handles, *config, *mqtt_abstraction);
                config = std::make_unique<ManagerConfig>(ms);
                modules_started = false;
                restart_modules = true;
            } else if (payload.at("method") == "check_config") {
                const std::string check_config_file_path = payload.at("params");

                try {
                    // check the config
                    auto cfg = ManagerConfig(ManagerSettings(prefix_opt, check_config_file_path));
                    controller_handle.send_message({{"id", payload.at("id")}});
                } catch (const std::exception& e) {
                    controller_handle.send_message({{"result", e.what()}, {"id", payload.at("id")}});
                }
            } else {
                // unknown payload
                EVLOG_error << fmt::format("Received unknown command via controller ipc:\n{}\n... ignoring",
                                           payload.dump(DUMP_INDENT));
            }
        } else if (msg.status == controller_ipc::MESSAGE_RETURN_STATUS::ERROR) {
            fmt::print("Error in IPC communication with controller: {}\nExiting\n", msg.json.at("error").dump(2));
            return EXIT_FAILURE;
        } else {
            // TIMEOUT fall-through
        }
#endif
    }

    return EXIT_SUCCESS;
}
} // namespace

int main(int argc, char* argv[]) {
    po::options_description desc("EVerest manager");
    desc.add_options()("version", "Print version and exit");
    desc.add_options()("help,h", "produce help message");
    desc.add_options()("check", "Check and validate all config files and exit (0=success)");
    desc.add_options()("dump", po::value<std::string>(),
                       "Dump validated and augmented main config and all used module manifests into dir");
    desc.add_options()("dumpmanifests", po::value<std::string>(),
                       "Dump manifests of all modules into dir (even modules not used in config) and exit");
    desc.add_options()("prefix", po::value<std::string>(), "Prefix path of everest installation");
    desc.add_options()("standalone,s", po::value<std::vector<std::string>>()->multitoken(),
                       "Module ID(s) to not automatically start child processes for (those must be started manually to "
                       "make the framework start!).");
    desc.add_options()("ignore", po::value<std::vector<std::string>>()->multitoken(),
                       "Module ID(s) to ignore: Do not automatically start child processes and do not require that "
                       "they are started.");
    desc.add_options()("dontvalidateschema", "Don't validate json schema on every message");
    desc.add_options()("config", po::value<std::string>(),
                       "Full path to a config file.  If the file does not exist and has no extension, it will be "
                       "looked up in the default config directory");
    desc.add_options()("db", po::value<std::string>(), "Full path to the configuration database file");
    desc.add_options()("db-init", "Indicator to initialize the database if it does not contain a valid configuration. "
                                  "Requires --config and --db to be set.");
    desc.add_options()("status-fifo", po::value<std::string>()->default_value(""),
                       "Path to a named pipe, that shall be used for status updates from the manager");
    desc.add_options()("retain-topics", "Retain configuration MQTT topics setup by manager for inspection, by default "
                                        "these will be cleared after startup");
    desc.add_options()("mqtt_everest_prefix", po::value<std::string>(),
                       "Override the MQTT everest prefix (useful for running multiple instances in parallel)");

    po::variables_map vm;

    try {
        const auto default_logging_cfg =
            defaults::PREFIX / fs::path(defaults::SYSCONF_DIR) / defaults::NAMESPACE / defaults::LOGGING_CONFIG_NAME;
        if (fs::exists(default_logging_cfg)) {
            Logging::init(default_logging_cfg.string());
        }
        po::store(po::parse_command_line(argc, argv, desc), vm);
        po::notify(vm);

        if (vm.count("help") != 0) {
            desc.print(std::cout);
            return EXIT_SUCCESS;
        }

        if (vm.count("version") != 0) {
            std::string argv0;
            if (argc > 0) {
                argv0 = *argv;
            }
            std::cout << argv0 << " (" << PROJECT_NAME << " " << PROJECT_VERSION << " " << GIT_VERSION << ") "
                      << std::endl;
            return EXIT_SUCCESS;
        }

        return boot(vm);

    } catch (const BootException& e) {
        EVLOG_error << "Failed to start up everest:\n" << e.what();
        return EXIT_FAILURE;
    } catch (const std::exception& e) {
        EVLOG_error << "Main manager process exits because of caught exception:\n" << e.what();
        return EXIT_FAILURE;
    }
}
