// See the file "COPYING" in the main distribution directory for copyright.

#pragma once

#include "zeek/zeek-config.h"

#include <map>
#include <set>
#include <string_view>
#include <utility>

#include "zeek/Reporter.h"
#include "zeek/ZeekArgs.h"
#include "zeek/plugin/Plugin.h"
#include "zeek/util-types.h"

namespace zeek {

namespace cluster {
class Backend;

namespace detail {
class Event;
}
} // namespace cluster

namespace plugin {

// Macros that trigger plugin hooks. We put this into macros to short-cut the
// code for the most common case that no plugin defines the hook.

/**
 * Macro to trigger hooks without result.
 *
 * @param hook The \a plugin::HookType constant corresponding to the hook to trigger.
 *
 * @param method_call The \a Manager method corresponding to the hook.
 */
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define PLUGIN_HOOK_VOID(hook, method_call)                                                                            \
    {                                                                                                                  \
        if ( zeek::plugin_mgr->HavePluginForHook(zeek::plugin::hook) )                                                 \
            zeek::plugin_mgr->method_call;                                                                             \
    }

/**
 * Macro to trigger hooks that return a result.
 *
 * @param hook The \a plugin::HookType constant corresponding to the hook to trigger.
 *
 * @param method_call The \a Manager method corresponding to the hook.
 *
 * @param default_result: The result to use if there's no plugin implementing
 * the hook.
 */
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
#define PLUGIN_HOOK_WITH_RESULT(hook, method_call, default_result)                                                     \
    (zeek::plugin_mgr->HavePluginForHook(zeek::plugin::hook) ? zeek::plugin_mgr->method_call : (default_result))

/**
 * A singleton object managing all plugins.
 */
class Manager {
public:
    using bif_init_func = void (*)(Plugin*);
    using plugin_list = std::list<Plugin*>;
    using component_list = Plugin::component_list;
    using inactive_plugin_list = std::list<std::pair<std::string, std::string>>;

    /**
     * Constructor.
     */
    Manager();

    /**
     * Destructor.
     */
    virtual ~Manager();

    /**
     * Searches a set of directories for plugins. If a specified directory
     * does not contain a plugin itself, the method searches for plugins
     * recursively. For plugins found, the method makes them available for
     * later activation via ActivatePlugin().
     *
     * This must be called only before InitPluginsPreScript().
     *
     * @param dir The directory to search for plugins. Multiple directories
     * can be given by separating them with zeek::util::path_list_separator.
     */
    void SearchDynamicPlugins(const std::string& dir);

    /**
     * Load a dynamic plugin located at \a path.
     *
     * This method dlopen()'s the given path, verifies that the static current_plugin
     * variable is populated (usually done by the Plugin constructor) and calls
     * the following members on it:
     *
     *   ->SetDynamic(true)
     *   ->DoConfigure()
     *   ->InitializeComponents()
     *   ->InitPreScript()
     *
     * Note that ->SetPluginLocation() will be called with an empty base_dir,
     * unless the Manager::current_dir variable was set, in which case the
     * plugin's base_dir will be current_dir.
     *
     * @param path A concrete plugin to load, usually a .so file.
     *
     * @return A pointer to the loaded Plugin instance, or an error message if loading failed.
     */
    zeek::expected<Plugin*, std::string> LoadDynamicPlugin(const std::string& path);

    /**
     * Activates a plugin that SearchDynamicPlugins() has previously discovered.
     * Activating a plugin involves loading its dynamic module, making its
     * bifs available, and adding its script paths to ZEEKPATH.
     *
     * This attempts to activate the plugin immediately. If that fails for
     * some reason, we schedule it to be retried later with
     * ActivateDynamicPlugins().
     *
     * @param name The name of the plugin, as found previously by
    ·* SearchPlugin().
     */
    void ActivateDynamicPlugin(const std::string& name);

    /**
     * Activates plugins that SearchDynamicPlugins() has previously discovered,
     * including any that have failed to load in prior calls to
     * ActivateDynamicPlugin(). Aborts if any plugins fails to activate.
     *
     * @param all If true, activates all plugins that are found. If false,
     * activates only those that should always be activated unconditionally,
     * as specified via the ZEEK_PLUGIN_ACTIVATE environment variable.
     */
    void ActivateDynamicPlugins(bool all);

    /**
     * First-stage initialization of the manager. This is called early on
     * during Zeek's initialization, before any scripts are processed, and
     * forwards to the corresponding Plugin methods.
     */
    void InitPreScript();

    /**
     * Second-stage initialization of the manager. This is called in between
     * pre- and post-script to make BiFs available.
     */
    void InitBifs();

    /**
     * Third-stage initialization of the manager. This is called late during
     * Zeek's initialization after any scripts are processed, and forwards to
     * the corresponding Plugin methods.
     */
    void InitPostScript();

    /**
     * Fourth-stage initialization of the manager. This is called just before
     * enqueuing zeek_init(), after script analysis and optimization completed.
     * It forwards to the corresponding Plugin methods.
     */
    void InitPreExecution();

    /**
     * Finalizes all plugins at termination time. This forwards to the
     * corresponding Plugin \a Done() methods.
     */
    void FinishPlugins();

    /**
     * Returns a list of all available activated plugins. This includes all
     * that are compiled in statically, as well as those loaded dynamically
     * so far.
     */
    plugin_list ActivePlugins() const;

    /**
     * Returns a list of all dynamic plugins that have been found, yet not
     * activated. The returned list contains pairs of plugin name and base
     * directory. Note that because they aren't activated, that's all
     * information we have access to.
     */
    inactive_plugin_list InactivePlugins() const;

    /**
     * Returns a list of all available components, in any plugin, that
     * are derived from a specific class. The class is given as the
     * template parameter \c T.
     */
    template<class T>
    std::list<T*> Components() const;

    /**
     * Returns the (dynamic) plugin associated with a given filesystem
     * path. The path can be the plugin directory itself, or any path
     * inside it.
     */
    Plugin* LookupPluginByPath(std::string_view path);

    /**
     * Returns true if there's at least one plugin interested in a given
     * hook.
     *
     * @param The hook to check.
     *
     * @return True if there's a plugin for that hook.
     */
    bool HavePluginForHook(HookType hook) const {
        // Inline to avoid the function call.
        return hooks[hook] != nullptr;
    }

    /**
     * Returns all the hooks, with their priorities, that are currently
     * enabled for a given plugin.
     *
     * @param plugin The plugin to return the hooks for.
     */
    std::list<std::pair<HookType, int>> HooksEnabledForPlugin(const Plugin* plugin) const;

    /**
     * Enables a hook for a given plugin.
     *
     * hook: The hook to enable.
     *
     * plugin: The plugin defining the hook.
     *
     * prio: The priority to associate with the plugin for this hook.
     */
    void EnableHook(HookType hook, Plugin* plugin, int prio);

    /**
     * Disables a hook for a given plugin.
     *
     * hook: The hook to enable.
     *
     * plugin: The plugin that used to define the hook.
     */
    void DisableHook(HookType hook, Plugin* plugin);

    /**
     * Registers interest in an event by a plugin, even if there's no handler
     * for it. Normally a plugin receives events through HookQueueEvent()
     * only if Zeek actually has code to execute for it. By calling this
     * method, the plugin tells Zeek to raise the event even if there's no
     * corresponding handler; it will then go into HookQueueEvent() just as
     * any other.
     *
     * @param handler The event being interested in.
     *
     * @param plugin The plugin expressing interest.
     */
    void RequestEvent(EventHandlerPtr handler, Plugin* plugin);

    /**
     * Register interest in the destruction of a Obj instance. When Zeek's
     * reference counting triggers the objects destructor to run, the \a
     * HookObjDtor will be called.
     *
     * @param handler The object being interested in.
     *
     * @param plugin The plugin expressing interest.
     */
    void RequestObjDtor(Obj* obj, Plugin* plugin);

    // Hook entry functions.

    /**
     * Hook that gives plugins a chance to take over loading an input
     * file. This method must be called between InitPreScript() and
     * InitPostScript() for each input file Zeek is about to load, either
     * given on the command line or via @load script directives. The hook can
     * take over the file, in which case Zeek must not further process it
     * otherwise.
     *
     * @return 1 if a plugin took over the file and loaded it successfully; 0
     * if a plugin took over the file but had trouble loading it; and -1 if
     * no plugin was interested in the file at all.
     */
    virtual int HookLoadFile(const Plugin::LoadType type, const std::string& file, const std::string& resolved);

    /**
     * Hook that gives plugins a chance to take over loading an input file,
     * including replacing the file's content. This method must be called
     * between InitPreScript() and InitPostScript() for each input file Zeek is
     * about to load, either given on the command line or via @load script
     * directives. The hook can take over the file, in which case Zeek must not
     * further process it otherwise; or provide its content, in which case Zeek
     * must use that and ignore the original file.
     *
     * @return tuple where the first element is 1 if a plugin took over the
     * file; 0 if a plugin took over the file but had trouble loading it; and
     * -1 if no plugin was interested in the file at all.
     *
     * If the plugins takes over by returning 1, there are two cases: if the
     * second tuple element remains unset, the plugin handled the loading
     * completely internally; the caller must not process it any further.
     * Alternatively, the plugin may optionally return the actual content to
     * use for the file as a string through the tuple's second element. If so,
     * the caller must ignore the file on disk and use that provided content
     * instead (including when there's actually no physical file in place on
     * disk at all).
     */
    virtual std::pair<int, std::optional<std::string>> HookLoadFileExtended(const Plugin::LoadType type,
                                                                            const std::string& file,
                                                                            const std::string& resolved);

    /**
     * Hook that filters calls to a script function/event/hook.
     *
     * @param func The function to be called.
     *
     * @param parent The frame in which the function is being called.
     *
     * @param args The function call's arguments; they may be modified by the
     * method.
     *
     * @return If a plugin handled the call, a Val representing the result
     * to pass back to the interpreter (for void functions and events,
     * it may be any Val and must be ignored). If no plugin handled the call,
     * the method returns null.
     */
    std::pair<bool, ValPtr> HookCallFunction(const Func* func, zeek::detail::Frame* parent, Args* args) const;

    /**
     * Hook that filters the queuing of an event.
     *
     * @param event The event to be queued; it may be modified.
     *
     * @return Returns true if a plugin handled the queuing; in that case
     * the plugin will have taken ownership.
     */
    bool HookQueueEvent(Event* event) const;

    /**
     * Hook that informs plugins about an update in network time.
     *
     * @param network_time The new network time.
     */
    void HookUpdateNetworkTime(double network_time) const;

    /**
     * Hook that executes when a connection's initial analyzer tree
     * has been fully set up. The hook can manipulate the tree at this time,
     * for example by adding further analyzers.
     *
     * @param conn The connection.
     */
    void HookSetupAnalyzerTree(Connection* conn) const;

    /**
     * Hook that informs plugins that the event queue is being drained.
     */
    void HookDrainEvents() const;

    /**
     * Hook that informs plugins that an Obj is being destroyed. Will be
     * called only for objects that a plugin has expressed interest in.
     */
    void HookObjDtor(void* obj) const;

    /**
     * Hook into log initialization. This method will be called when a
     * logging writer is created. A writer represents a single logging
     * filter. The method is called in the main thread, on the node that
     * causes a log line to be written. It will _not_ be called on the logger
     * node. The function will be called once for every instantiated writer.
     *
     * @param writer The name of the writer being instantiated.
     *
     * @param instantiating_filter Name of the filter causing the
     *        writer instantiation.
     *
     * @param local True if the filter is logging locally (writer
     *              thread will be located in same process).
     *
     * @param remote True if filter is logging remotely (writer thread
     *               will be located in different thread, typically
     *               in manager or logger node).
     *
     * @param info WriterBackend::WriterInfo with information about the writer.
     *
     * @param num_fields number of fields in the record being written.
     *
     * @param fields threading::Field description of the fields being logged.
     */
    void HookLogInit(const std::string& writer, const std::string& instantiating_filter, bool local, bool remote,
                     const logging::WriterBackend::WriterInfo& info, int num_fields,
                     const threading::Field* const* fields) const;

    /**
     * Hook into log writing. This method will be called for each log line
     * being written by each writer. Each writer represents a single logging
     * filter. The method is called in the main thread, on the node that
     * causes a log line to be written. It will _not_ be called on the logger
     * node.
     * This function allows plugins to modify or skip logging of information.
     * Note - once a log line is skipped (by returning false), it will not
     * passed on to hooks that have not yet been called.
     *
     * @param writer The name of the writer.
     *
     * @param filter Name of the filter being written to.
     *
     * @param info WriterBackend::WriterInfo with information about the writer.
     *
     * @param num_fields number of fields in the record being written.
     *
     * @param fields threading::Field description of the fields being logged.
     *
     * @param vals threading::Values containing the values being written. Values
     *             can be modified in the Hook.
     *
     * @return true if log line should be written, false if log line should be
     *         skipped and not passed on to the writer.
     */
    bool HookLogWrite(const std::string& writer, const std::string& filter,
                      const logging::WriterBackend::WriterInfo& info, int num_fields,
                      const threading::Field* const* fields, threading::Value** vals) const;

    /**
     * Hook into reporting. This method will be called for each reporter call
     * made; this includes weirds. The method cannot manipulate the data at
     * the current time; however it is possible to prevent script-side events
     * from being called by returning false.
     *
     * @param prefix The prefix passed by the reporter framework
     *
     * @param event The event to be called
     *
     * @param conn The associated connection
     *
     * @param addl Additional Zeek values; typically will be passed to the event
     *             by the reporter framework.
     *
     * @param location True if event expects location information
     *
     * @param location1 First location
     *
     * @param location2 Second location
     *
     * @param time True if event expects time information
     *
     * @param message Message supplied by the reporter framework
     *
     * @return true if event should be called by the reporter framework, false
     *         if the event call should be skipped
     */
    bool HookReporter(const std::string& prefix, const EventHandlerPtr event, const Connection* conn,
                      const ValPList* addl, bool location, const zeek::detail::Location* location1,
                      const zeek::detail::Location* location2, bool time, const std::string& message);

    /**
     * Hook for packets that are considered unprocessed by an Analyzer. This
     * typically means that a packet has not had a log entry written for it by
     * the time analysis finishes.
     *
     * @param packet The data for an unprocessed packet
     */
    void HookUnprocessedPacket(const Packet* packet) const;

    /**
     * Hook for intercepting remote event publish operations.
     *
     * This hook is invoked when Cluster::publish(), Cluster::publish_hrw() or
     * Cluster::publish_rr() is used in the scripting layer to publish a remote
     * event to a topic. It is also invoked when calling PublishEvent() on the
     * active cluster backend directly from C++ plugins. This hook can be used
     * for metrics collection, modifying or redirecting events to a different
     * topic. It's event possible to translate an event to another one. A plugin
     * should return false if it took responsibility of publishing the event, or
     * the verdict is to skip the publish operation.
     *
     * @param backend The backend publishing this event
     * @param topic The topic to which to publish this event
     * @param event The event itself
     *
     * @return true if event should be published, false if the publish
     *         operation should be skipped.
     */
    bool HookPublishEvent(zeek::cluster::Backend& backend, const std::string& topic, zeek::cluster::Event& event) const;

    /**
     * Internal method that registers a freshly instantiated plugin with
     * the manager.
     *
     * @param plugin The plugin to register. The method does not take
     * ownership, yet assumes the pointer will stay valid at least until
     * the Manager is destroyed.
     */
    static void RegisterPlugin(Plugin* plugin);

    /**
     * Internal method that registers a bif file's init function for a
     * plugin.
     *
     * @param plugin The plugin to register the function for.
     *
     * @param c The init function to register.
     */
    static void RegisterBifFile(const char* plugin, bif_init_func c);

    /**
     * Adds the paths from built-in plugins (e.g. from --include-plugins)
     * to ZEEKPATH so they are loaded correctly.
     */
    void ExtendZeekPathForPlugins();

private:
    bool ActivateDynamicPluginInternal(const std::string& name, bool ok_if_not_found, std::vector<std::string>* errors);
    void UpdateInputFiles();
    void MetaHookPre(HookType hook, const HookArgumentList& args) const;
    void MetaHookPost(HookType hook, const HookArgumentList& args, const HookArgument& result) const;

    // Directories that have already been searched for dynamic plugins.
    // Used to prevent multiple searches of the same dirs (e.g. via symlinks).
    // The paths stored in the set are made canonical via calls to
    // std::filesystem::canonical().
    std::set<std::string, std::less<>> searched_dirs;

    // Plugins that were explicitly requested to be activated, but failed to
    // load at first.
    std::set<std::string> requested_plugins;

    // All found dynamic plugins, mapping their names to base directory.
    using dynamic_plugin_map = std::map<std::string, std::string>;
    dynamic_plugin_map dynamic_plugins;

    // We temporarily buffer scripts to load to get them to load in the
    // right order.
    using file_list = std::list<std::string>;
    file_list scripts_to_load;

    bool init; // Flag indicating whether InitPreScript() has run yet.

    // A hook list keeps pairs of plugin and priority interested in a
    // given hook.
    using hook_list = std::list<std::pair<int, Plugin*>>;

    // An array indexed by HookType. An entry is null if there's no hook
    // of that type enabled.
    hook_list** hooks;

    // A map of all the top-level plugin directories.
    std::map<std::string, Plugin*> plugins_by_path;

    // Helpers providing access to current state during dlopen().
    static Plugin* current_plugin;
    static const char* current_dir;
    static const char* current_sopath;

    // Returns a modifiable list of all plugins, both static and dynamic.
    // This is a static method so that plugins can register themselves
    // even before the manager exists.
    static plugin_list* ActivePluginsInternal();

    using bif_init_func_list = std::list<bif_init_func>;
    using bif_init_func_map = std::map<std::string, bif_init_func_list*>;

    // Returns a modifiable map of all bif files. This is a static method
    // so that plugins can register their bifs even before the manager
    // exists.
    static bif_init_func_map* BifFilesInternal();
};

template<class T>
std::list<T*> Manager::Components() const {
    std::list<T*> result;

    for ( plugin_list::const_iterator p = ActivePluginsInternal()->begin(); p != ActivePluginsInternal()->end(); p++ ) {
        component_list components = (*p)->Components();

        for ( component_list::const_iterator c = components.begin(); c != components.end(); c++ ) {
            T* t = dynamic_cast<T*>(*c);

            if ( t )
                result.push_back(t);
        }
    }

    return result;
}

namespace detail {

/**
 * Internal class used by bifcl-generated code to register its init functions at runtime.
 */
class __RegisterBif {
public:
    __RegisterBif(const char* plugin, Manager::bif_init_func init) { Manager::RegisterBifFile(plugin, init); }
};

} // namespace detail
} // namespace plugin

ZEEK_EXTERN_DATA plugin::Manager* plugin_mgr;

} // namespace zeek
