"""
The Salt loader is the core to Salt's plugin system, the loader scans
directories for python loadable code and organizes the code into the
plugin interfaces used by Salt.
"""

import contextlib
import inspect
import logging
import os
import re
import time
import types

import salt.config
import salt.defaults.events
import salt.defaults.exitcodes
import salt.loader.context
import salt.syspaths
import salt.utils.context
import salt.utils.data
import salt.utils.dictupdate
import salt.utils.files
import salt.utils.lazy
import salt.utils.odict
import salt.utils.platform
import salt.utils.stringutils
import salt.utils.versions
from salt.exceptions import LoaderError
from salt.template import check_render_pipe_str
from salt.utils import entrypoints

from .lazy import SALT_BASE_PATH, FilterDictWrapper, LazyLoader

log = logging.getLogger(__name__)

# Because on the cloud drivers we do `from salt.cloud.libcloudfuncs import *`
# which simplifies code readability, it adds some unsupported functions into
# the driver's module scope.
# We list un-supported functions here. These will be removed from the loaded.
#  TODO:  remove the need for this cross-module code. Maybe use NotImplemented
LIBCLOUD_FUNCS_NOT_SUPPORTED = (
    "parallels.avail_sizes",
    "parallels.avail_locations",
    "proxmox.avail_sizes",
)

SALT_INTERNAL_LOADERS_PATHS = (
    str(SALT_BASE_PATH / "auth"),
    str(SALT_BASE_PATH / "beacons"),
    str(SALT_BASE_PATH / "cache"),
    str(SALT_BASE_PATH / "client" / "ssh" / "wrapper"),
    str(SALT_BASE_PATH / "cloud" / "clouds"),
    str(SALT_BASE_PATH / "engines"),
    str(SALT_BASE_PATH / "executors"),
    str(SALT_BASE_PATH / "fileserver"),
    str(SALT_BASE_PATH / "grains"),
    str(SALT_BASE_PATH / "log_handlers"),
    str(SALT_BASE_PATH / "matchers"),
    str(SALT_BASE_PATH / "metaproxy"),
    str(SALT_BASE_PATH / "modules"),
    str(SALT_BASE_PATH / "netapi"),
    str(SALT_BASE_PATH / "output"),
    str(SALT_BASE_PATH / "pillar"),
    str(SALT_BASE_PATH / "proxy"),
    str(SALT_BASE_PATH / "queues"),
    str(SALT_BASE_PATH / "renderers"),
    str(SALT_BASE_PATH / "returners"),
    str(SALT_BASE_PATH / "roster"),
    str(SALT_BASE_PATH / "runners"),
    str(SALT_BASE_PATH / "sdb"),
    str(SALT_BASE_PATH / "serializers"),
    str(SALT_BASE_PATH / "spm" / "pkgdb"),
    str(SALT_BASE_PATH / "spm" / "pkgfiles"),
    str(SALT_BASE_PATH / "states"),
    str(SALT_BASE_PATH / "thorium"),
    str(SALT_BASE_PATH / "tokens"),
    str(SALT_BASE_PATH / "tops"),
    str(SALT_BASE_PATH / "utils"),
    str(SALT_BASE_PATH / "wheel"),
)


def static_loader(
    opts,
    ext_type,
    tag,
    pack=None,
    int_type=None,
    ext_dirs=True,
    ext_type_dirs=None,
    base_path=None,
    filter_name=None,
    loaded_base_name=None,
):
    funcs = LazyLoader(
        _module_dirs(
            opts,
            ext_type,
            tag,
            int_type,
            ext_dirs,
            ext_type_dirs,
            base_path,
        ),
        opts,
        tag=tag,
        pack=pack,
        loaded_base_name=loaded_base_name,
    )
    ret = {}
    funcs._load_all()
    if filter_name:
        funcs = FilterDictWrapper(funcs, filter_name)
    for key in funcs:
        ret[key] = funcs[key]
    return ret


def _module_dirs(
    opts,
    ext_type,
    tag=None,
    int_type=None,
    ext_dirs=True,
    ext_type_dirs=None,
    base_path=None,
    load_extensions=True,
):
    if tag is None:
        tag = ext_type
    sys_types = [os.path.join(base_path or str(SALT_BASE_PATH), int_type or ext_type)]

    if opts.get("extension_modules"):
        ext_types = [os.path.join(opts["extension_modules"], ext_type)]
    else:
        ext_types = []

    if not sys_types[0].startswith(SALT_INTERNAL_LOADERS_PATHS):
        raise RuntimeError(
            "{!r} is not considered a salt internal loader path. If this "
            "is a new loader being added, please also add it to "
            "{}.SALT_INTERNAL_LOADERS_PATHS.".format(sys_types[0], __name__)
        )

    ext_type_types = []
    if ext_dirs:
        if ext_type_dirs is None:
            ext_type_dirs = f"{tag}_dirs"
        if ext_type_dirs in opts:
            ext_type_types.extend(opts[ext_type_dirs])
        if ext_type_dirs and load_extensions is True:
            for entry_point in entrypoints.iter_entry_points("salt.loader"):
                with catch_entry_points_exception(entry_point) as ctx:
                    loaded_entry_point = entry_point.load()
                if ctx.exception_caught:
                    continue

                # Old way of defining loader entry points
                #   [options.entry_points]
                #   salt.loader=
                #     runner_dirs = thirpartypackage.loader:func_to_get_list_of_dirs
                #     module_dirs = thirpartypackage.loader:func_to_get_list_of_dirs
                #
                #
                # New way of defining entrypoints
                #   [options.entry_points]
                #   salt.loader=
                #     <this-name-does-not-matter> = thirpartypackage
                #     <this-name-does-not-matter> = thirpartypackage:callable
                #
                # We try and see if the thirpartypackage has a `ext_type` sub module, and if so,
                # we append it to loaded_entry_point_paths.
                # If the entry-point is in the form of `thirpartypackage:callable`, the return of that
                # callable must be a dictionary where the keys are the `ext_type`'s and the values must be
                # lists of paths.

                # We could feed the paths we load directly to `ext_type_types`, but we would not
                # check for duplicates
                loaded_entry_point_paths = set()

                if isinstance(loaded_entry_point, types.FunctionType):
                    # If the entry point object is a function, we have two scenarios
                    #   1: It returns a list; This is an old style entry entry_point
                    #   2: It returns a dictionary; This is a new style entry point
                    with catch_entry_points_exception(entry_point) as ctx:
                        loaded_entry_point_value = loaded_entry_point()
                    if ctx.exception_caught:
                        continue

                    if isinstance(loaded_entry_point_value, dict):
                        # This is new style entry-point and it returns a dictionary.
                        # It MUST contain `ext_type` in it's keys to be considered
                        if ext_type not in loaded_entry_point_value:
                            continue
                        with catch_entry_points_exception(entry_point) as ctx:
                            if isinstance(loaded_entry_point_value[ext_type], str):
                                # No strings please!
                                raise ValueError(
                                    "The callable must return an iterable of strings. "
                                    "A single string is not supported."
                                )
                            for path in loaded_entry_point_value[ext_type]:
                                loaded_entry_point_paths.add(path)
                    else:
                        # This is old style entry-point, and, as such, the entry point name MUST
                        # match the value of `ext_type_dirs
                        if entry_point.name != ext_type_dirs:
                            continue
                        for path in loaded_entry_point_value:
                            loaded_entry_point_paths.add(path)
                elif isinstance(loaded_entry_point, types.ModuleType):
                    # This is a new style entry points definition which just points us to a package
                    #
                    # We try and see if the thirpartypackage has a `ext_type` sub module, and if so,
                    # we append it to loaded_entry_point_paths.
                    for loaded_entry_point_path in loaded_entry_point.__path__:
                        with catch_entry_points_exception(entry_point) as ctx:
                            entry_point_ext_type_package_path = os.path.join(
                                loaded_entry_point_path, ext_type
                            )
                            if not os.path.exists(entry_point_ext_type_package_path):
                                continue
                        if ctx.exception_caught:
                            continue
                        loaded_entry_point_paths.add(entry_point_ext_type_package_path)
                else:
                    with catch_entry_points_exception(entry_point):
                        raise ValueError(
                            "Don't know how to load a salt extension from {}".format(
                                loaded_entry_point
                            )
                        )

                # Finally, we check all paths that we collected to see if they exist
                for path in loaded_entry_point_paths:
                    if os.path.exists(path):
                        ext_type_types.append(path)

    cli_module_dirs = []
    # The dirs can be any module dir, or a in-tree _{ext_type} dir
    for _dir in opts.get("module_dirs", []):
        # Prepend to the list to match cli argument ordering
        maybe_dir = os.path.join(_dir, ext_type)
        if os.path.isdir(maybe_dir):
            cli_module_dirs.insert(0, maybe_dir)
            continue

        maybe_dir = os.path.join(_dir, f"_{ext_type}")
        if os.path.isdir(maybe_dir):
            cli_module_dirs.insert(0, maybe_dir)

    if opts.get("features", {}).get(
        "enable_deprecated_module_search_path_priority", False
    ):
        salt.utils.versions.warn_until(
            3008,
            "The old module search path priority will be removed in Salt 3008. "
            "For more information see https://github.com/saltstack/salt/pull/65938.",
        )
        return cli_module_dirs + ext_type_types + ext_types + sys_types
    else:
        return cli_module_dirs + ext_types + ext_type_types + sys_types


def minion_mods(
    opts,
    context=None,
    utils=None,
    whitelist=None,
    initial_load=False,
    loaded_base_name=None,
    notify=False,
    static_modules=None,
    proxy=None,
    file_client=None,
):
    """
    Load execution modules

    Returns a dictionary of execution modules appropriate for the current
    system by evaluating the __virtual__() function in each module.

    :param dict opts: The Salt options dictionary

    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__

    :param dict utils: Utility functions which should be made available to
                            Salt modules in __utils__. See `utils_dirs` in
                            salt.config for additional information about
                            configuration.

    :param list whitelist: A list of modules which should be whitelisted.
    :param bool initial_load: Deprecated flag! Unused.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    :param bool notify: Flag indicating that an event should be fired upon
                        completion of module loading.


    Example:

    .. code-block:: python

        import salt.config
        import salt.loader

        __opts__ = salt.config.minion_config('/etc/salt/minion')
        __grains__ = salt.loader.grains(__opts__)
        __opts__['grains'] = __grains__
        __utils__ = salt.loader.utils(__opts__)
        __salt__ = salt.loader.minion_mods(__opts__, utils=__utils__)
        __salt__['test.ping']()
    """
    # TODO Publish documentation for module whitelisting
    if not whitelist:
        whitelist = opts.get("whitelist_modules", None)
    ret = LazyLoader(
        _module_dirs(opts, "modules", "module"),
        opts,
        tag="module",
        pack={
            "__context__": context,
            "__utils__": utils,
            "__proxy__": proxy,
            "__opts__": opts,
            "__file_client__": file_client,
        },
        whitelist=whitelist,
        loaded_base_name=loaded_base_name,
        static_modules=static_modules,
        extra_module_dirs=utils.module_dirs if utils else None,
        pack_self="__salt__",
    )

    # Allow the usage of salt dunder in utils modules.
    if utils and isinstance(utils, LazyLoader):
        utils.pack["__salt__"] = ret

    # Load any provider overrides from the configuration file providers option
    #  Note: Providers can be pkg, service, user or group - not to be confused
    #        with cloud providers.
    providers = opts.get("providers", False)
    if providers and isinstance(providers, dict):
        for mod in providers:
            # sometimes providers opts is not to diverge modules but
            # for other configuration
            try:
                funcs = raw_mod(opts, providers[mod], ret)
            except TypeError:
                break
            else:
                if funcs:
                    for func in funcs:
                        f_key = "{}{}".format(mod, func[func.rindex(".") :])
                        ret[f_key] = funcs[func]

    if notify:
        import salt.utils.event

        with salt.utils.event.get_event("minion", opts=opts, listen=False) as evt:
            evt.fire_event(
                {"complete": True}, tag=salt.defaults.events.MINION_MOD_REFRESH_COMPLETE
            )

    return ret


def raw_mod(opts, name, functions, mod="modules", loaded_base_name=None):
    """
    Returns a single module loaded raw and bypassing the __virtual__ function

    :param dict opts: The Salt options dictionary
    :param str name: The name of the module to load
    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param str mod: The extension type.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.

    Example:

    .. code-block:: python

        import salt.config
        import salt.loader

        __opts__ = salt.config.minion_config('/etc/salt/minion')
        testmod = salt.loader.raw_mod(__opts__, 'test', None)
        testmod['test.ping']()
    """
    loader = LazyLoader(
        _module_dirs(opts, mod, "module"),
        opts,
        tag="rawmodule",
        virtual_enable=False,
        pack={"__salt__": functions},
        loaded_base_name=loaded_base_name,
    )
    # if we don't have the module, return an empty dict
    if name not in loader.file_mapping:
        return {}

    # load a single module (the one passed in)
    loader._load_module(name)
    # return a copy of *just* the funcs for `name`
    return dict({x: loader[x] for x in loader._dict})


def metaproxy(opts, loaded_base_name=None):
    """
    Return functions used in the meta proxy

    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(opts, "metaproxy"),
        opts,
        tag="metaproxy",
        loaded_base_name=loaded_base_name,
    )


def matchers(opts, loaded_base_name=None):
    """
    Return the matcher services plugins

    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(opts, "matchers"),
        opts,
        tag="matchers",
        loaded_base_name=loaded_base_name,
    )


def engines(opts, functions, runners, utils, proxy=None, loaded_base_name=None):
    """
    Return the engines plugins

    :param dict opts: The Salt options dictionary
    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param LazyLoader runners: A LazyLoader instance returned from ``runner``.
    :param LazyLoader utils: A LazyLoader instance returned from ``utils``.
    :param LazyLoader proxy: An optional LazyLoader instance returned from ``proxy``.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    pack = {
        "__salt__": functions,
        "__runners__": runners,
        "__proxy__": proxy,
        "__utils__": utils,
    }
    return LazyLoader(
        _module_dirs(opts, "engines"),
        opts,
        tag="engines",
        pack=pack,
        extra_module_dirs=utils.module_dirs if utils else None,
        loaded_base_name=loaded_base_name,
    )


def proxy(
    opts,
    functions=None,
    returners=None,
    whitelist=None,
    utils=None,
    context=None,
    pack_self="__proxy__",
    loaded_base_name=None,
):
    """
    Returns the proxy module for this salt-proxy-minion

    :param dict opts: The Salt options dictionary
    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param LazyLoader returners: A LazyLoader instance returned from ``returners``.
    :param LazyLoader utils: A LazyLoader instance returned from ``utils``.
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(opts, "proxy"),
        opts,
        tag="proxy",
        pack={
            "__salt__": functions,
            "__ret__": returners,
            "__utils__": utils,
            "__context__": context,
        },
        extra_module_dirs=utils.module_dirs if utils else None,
        pack_self=pack_self,
        loaded_base_name=loaded_base_name,
    )


def returners(
    opts, functions, whitelist=None, context=None, proxy=None, loaded_base_name=None
):
    """
    Returns the returner modules

    :param dict opts: The Salt options dictionary
    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param list whitelist: A list of modules which should be whitelisted.
    :param LazyLoader proxy: An optional LazyLoader instance returned from ``proxy``.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(opts, "returners", "returner"),
        opts,
        tag="returner",
        whitelist=whitelist,
        pack={"__salt__": functions, "__context__": context, "__proxy__": proxy or {}},
        loaded_base_name=loaded_base_name,
    )


def utils(
    opts,
    whitelist=None,
    context=None,
    proxy=None,
    file_client=None,
    pack_self=None,
    loaded_base_name=None,
):
    """
    Returns the utility modules

    :param dict opts: The Salt options dictionary
    :param list whitelist: A list of modules which should be whitelisted.
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param LazyLoader proxy: An optional LazyLoader instance returned from ``proxy``.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(opts, "utils", ext_type_dirs="utils_dirs", load_extensions=False),
        opts,
        tag="utils",
        whitelist=whitelist,
        pack={
            "__context__": context,
            "__proxy__": proxy or {},
            "__file_client__": file_client,
        },
        pack_self=pack_self,
        loaded_base_name=loaded_base_name,
        _only_pack_properly_namespaced_functions=False,
    )


def pillars(opts, functions, context=None, loaded_base_name=None):
    """
    Returns the pillars modules

    :param dict opts: The Salt options dictionary
    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    _utils = utils(opts)
    ret = LazyLoader(
        _module_dirs(opts, "pillar"),
        opts,
        tag="pillar",
        pack={"__salt__": functions, "__context__": context, "__utils__": _utils},
        extra_module_dirs=_utils.module_dirs,
        pack_self="__ext_pillar__",
        loaded_base_name=loaded_base_name,
    )
    return FilterDictWrapper(ret, ".ext_pillar")


def tops(opts, loaded_base_name=None):
    """
    Returns the tops modules

    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    if "master_tops" not in opts:
        return {}
    whitelist = list(opts["master_tops"].keys())
    ret = LazyLoader(
        _module_dirs(opts, "tops", "top"),
        opts,
        tag="top",
        whitelist=whitelist,
        loaded_base_name=loaded_base_name,
    )
    return FilterDictWrapper(ret, ".top")


def wheels(opts, whitelist=None, context=None, loaded_base_name=None):
    """
    Returns the wheels modules

    :param dict opts: The Salt options dictionary
    :param list whitelist: A list of modules which should be whitelisted.
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    if context is None:
        context = {}
    return LazyLoader(
        _module_dirs(opts, "wheel"),
        opts,
        tag="wheel",
        whitelist=whitelist,
        pack={"__context__": context},
        loaded_base_name=loaded_base_name,
    )


def outputters(opts, loaded_base_name=None):
    """
    Returns the outputters modules

    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    :returns: LazyLoader instance, with only outputters present in the keyspace
    """
    ret = LazyLoader(
        _module_dirs(opts, "output", ext_type_dirs="outputter_dirs"),
        opts,
        tag="output",
        loaded_base_name=loaded_base_name,
    )
    wrapped_ret = FilterDictWrapper(ret, ".output")
    # TODO: this name seems terrible... __salt__ should always be execution mods
    ret.pack["__salt__"] = wrapped_ret
    return wrapped_ret


def serializers(opts, loaded_base_name=None):
    """
    Returns the serializers modules
    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    :returns: LazyLoader instance, with only serializers present in the keyspace
    """
    return LazyLoader(
        _module_dirs(opts, "serializers"),
        opts,
        tag="serializers",
        loaded_base_name=loaded_base_name,
    )


def eauth_tokens(opts, loaded_base_name=None):
    """
    Returns the tokens modules
    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    :returns: LazyLoader instance, with only token backends present in the keyspace
    """
    return LazyLoader(
        _module_dirs(opts, "tokens"),
        opts,
        tag="tokens",
        loaded_base_name=loaded_base_name,
    )


def auth(opts, whitelist=None, loaded_base_name=None):
    """
    Returns the auth modules

    :param dict opts: The Salt options dictionary

    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param list whitelist: A list of modules which should be whitelisted.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    :returns: LazyLoader
    """
    return LazyLoader(
        _module_dirs(opts, "auth"),
        opts,
        tag="auth",
        whitelist=whitelist,
        pack={"__salt__": minion_mods(opts)},
        loaded_base_name=loaded_base_name,
    )


def fileserver(opts, backends, loaded_base_name=None):
    """
    Returns the file server modules

    :param dict opts: The Salt options dictionary
    :param list backends: List of backends to load.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    _utils = utils(opts)

    if backends is not None:
        if not isinstance(backends, list):
            backends = [backends]

        # If backend is a VCS, add both the '-fs' and non '-fs' versions to the list.
        # Use a set to keep them unique
        backend_set = set()
        vcs_re = re.compile("^(git|svn|hg)(?:fs)?$")
        for backend in backends:
            match = vcs_re.match(backend)
            if match:
                backend_set.add(match.group(1))
                backend_set.add(match.group(1) + "fs")
            else:
                backend_set.add(backend)
        backends = list(backend_set)

    return LazyLoader(
        _module_dirs(opts, "fileserver"),
        opts,
        tag="fileserver",
        whitelist=backends,
        pack={"__utils__": _utils},
        extra_module_dirs=_utils.module_dirs,
        loaded_base_name=loaded_base_name,
    )


def roster(opts, runner=None, utils=None, whitelist=None, loaded_base_name=None):
    """
    Returns the roster modules

    :param dict opts: The Salt options dictionary
    :param LazyLoader runner: A LazyLoader instance returned from ``runner``.
    :param LazyLoader utils: A LazyLoader instance returned from ``utils``.
    :param list whitelist: A list of modules which should be whitelisted.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(opts, "roster"),
        opts,
        tag="roster",
        whitelist=whitelist,
        pack={"__runner__": runner, "__utils__": utils},
        extra_module_dirs=utils.module_dirs if utils else None,
        loaded_base_name=loaded_base_name,
    )


def thorium(opts, functions, runners, loaded_base_name=None):
    """
    Load the thorium runtime modules

    :param dict opts: The Salt options dictionary
    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param LazyLoader runners: A LazyLoader instance returned from ``runner``.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    pack = {"__salt__": functions, "__runner__": runners, "__context__": {}}
    ret = LazyLoader(
        _module_dirs(opts, "thorium"),
        opts,
        tag="thorium",
        pack=pack,
        loaded_base_name=loaded_base_name,
    )
    ret.pack["__thorium__"] = ret
    return ret


def states(
    opts,
    functions,
    utils,
    serializers,
    whitelist=None,
    proxy=None,
    context=None,
    loaded_base_name=None,
    file_client=None,
):
    """
    Returns the state modules

    :param dict opts: The Salt options dictionary
    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param LazyLoader runners: A LazyLoader instance returned from ``runner``.
    :param LazyLoader utils: A LazyLoader instance returned from ``utils``.
    :param LazyLoader serializers: An optional LazyLoader instance returned from ``serializers``.
    :param LazyLoader proxy: An optional LazyLoader instance returned from ``proxy``.
    :param list whitelist: A list of modules which should be whitelisted.
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.

    .. code-block:: python

        import salt.config
        import salt.loader

        __opts__ = salt.config.minion_config('/etc/salt/minion')
        statemods = salt.loader.states(__opts__, None, None)
    """
    if context is None:
        context = {}

    return LazyLoader(
        _module_dirs(opts, "states"),
        opts,
        tag="states",
        pack={
            "__salt__": functions,
            "__proxy__": proxy or {},
            "__utils__": utils,
            "__serializers__": serializers,
            "__context__": context,
            "__file_client__": file_client,
        },
        whitelist=whitelist,
        extra_module_dirs=utils.module_dirs if utils else None,
        pack_self="__states__",
        loaded_base_name=loaded_base_name,
    )


def beacons(opts, functions, context=None, proxy=None, loaded_base_name=None):
    """
    Load the beacon modules

    :param dict opts: The Salt options dictionary
    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param LazyLoader proxy: An optional LazyLoader instance returned from ``proxy``.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(opts, "beacons"),
        opts,
        tag="beacons",
        pack={"__context__": context, "__salt__": functions, "__proxy__": proxy or {}},
        virtual_funcs=[],
        loaded_base_name=loaded_base_name,
    )


def log_handlers(opts, loaded_base_name=None):
    """
    Returns the custom logging handler modules

    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    ret = LazyLoader(
        _module_dirs(
            opts,
            "log_handlers",
        ),
        opts,
        tag="log_handlers",
        loaded_base_name=loaded_base_name,
    )
    return FilterDictWrapper(ret, ".setup_handlers")


def ssh_wrapper(
    opts, functions=None, context=None, file_client=None, loaded_base_name=None
):
    """
    Returns the custom logging handler modules

    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(
            opts,
            "wrapper",
            base_path=str(SALT_BASE_PATH / "client" / "ssh"),
        ),
        opts,
        tag="wrapper",
        pack={
            "__salt__": functions,
            "__context__": context,
            "__file_client__": file_client,
        },
        loaded_base_name=loaded_base_name,
    )


def render(
    opts,
    functions,
    states=None,
    proxy=None,
    context=None,
    file_client=None,
    loaded_base_name=None,
):
    """
    Returns the render modules

    :param dict opts: The Salt options dictionary
    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param LazyLoader states: A LazyLoader instance returned from ``states``.
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param LazyLoader proxy: An optional LazyLoader instance returned from ``proxy``.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    if context is None:
        context = {}

    pack = {
        "__salt__": functions,
        "__grains__": opts.get("grains", {}),
        "__context__": context,
        "__file_client__": file_client,
    }

    if states:
        pack["__states__"] = states

    if proxy is None:
        proxy = {}
    pack["__proxy__"] = proxy

    ret = LazyLoader(
        _module_dirs(
            opts,
            "renderers",
            "render",
            ext_type_dirs="render_dirs",
        ),
        opts,
        tag="render",
        pack=pack,
        loaded_base_name=loaded_base_name,
    )
    rend = FilterDictWrapper(ret, ".render")

    if not check_render_pipe_str(
        opts["renderer"], rend, opts["renderer_blacklist"], opts["renderer_whitelist"]
    ):
        err = (
            "The renderer {} is unavailable, this error is often because "
            "the needed software is unavailable".format(opts["renderer"])
        )
        log.critical(err)
        raise LoaderError(err)
    return rend


def grain_funcs(opts, proxy=None, context=None, loaded_base_name=None):
    """
    Returns the grain functions

    :param dict opts: The Salt options dictionary
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param LazyLoader proxy: An optional LazyLoader instance returned from ``proxy``.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.

      .. code-block:: python

          import salt.config
          import salt.loader

          __opts__ = salt.config.minion_config('/etc/salt/minion')
          grainfuncs = salt.loader.grain_funcs(__opts__)
    """
    _utils = utils(opts, proxy=proxy)
    ret = LazyLoader(
        _module_dirs(
            opts,
            "grains",
            "grain",
            ext_type_dirs="grains_dirs",
        ),
        opts,
        tag="grains",
        extra_module_dirs=_utils.module_dirs,
        pack={"__utils__": _utils, "__context__": context},
        loaded_base_name=loaded_base_name,
    )
    return ret


def _format_cached_grains(cached_grains):
    """
    Returns cached grains with fixed types, like tuples.
    """
    if cached_grains.get("osrelease_info"):
        osrelease_info = cached_grains["osrelease_info"]
        if isinstance(osrelease_info, list):
            cached_grains["osrelease_info"] = tuple(osrelease_info)
    return cached_grains


def _load_cached_grains(opts, cfn):
    """
    Returns the grains cached in cfn, or None if the cache is too old or is
    corrupted.
    """
    if not os.path.isfile(cfn):
        log.debug("Grains cache file does not exist.")
        return None

    grains_cache_age = int(time.time() - os.path.getmtime(cfn))
    if grains_cache_age > opts.get("grains_cache_expiration", 300):
        log.debug(
            "Grains cache last modified %s seconds ago and cache "
            "expiration is set to %s. Grains cache expired. "
            "Refreshing.",
            grains_cache_age,
            opts.get("grains_cache_expiration", 300),
        )
        return None

    if opts.get("refresh_grains_cache", False):
        log.debug("refresh_grains_cache requested, Refreshing.")
        return None

    log.debug("Retrieving grains from cache")
    try:
        with salt.utils.files.fopen(cfn, "rb") as fp_:
            cached_grains = salt.utils.data.decode(
                salt.payload.load(fp_), preserve_tuples=True
            )
        if not cached_grains:
            log.debug("Cached grains are empty, cache might be corrupted. Refreshing.")
            return None

        return _format_cached_grains(cached_grains)
    except OSError:
        return None


def grains(opts, force_refresh=False, proxy=None, context=None, loaded_base_name=None):
    """
    Return the functions for the dynamic grains and the values for the static
    grains.

    :param dict opts: The Salt options dictionary
    :param bool force_refresh: Force the refresh of grains
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param LazyLoader proxy: An optional LazyLoader instance returned from ``proxy``.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.

    Since grains are computed early in the startup process, grains functions
    do not have __salt__ or __proxy__ available.  At proxy-minion startup,
    this function is called with the proxymodule LazyLoader object so grains
    functions can communicate with their controlled device.

    .. code-block:: python

        import salt.config
        import salt.loader

        __opts__ = salt.config.minion_config('/etc/salt/minion')
        __grains__ = salt.loader.grains(__opts__)
        print __grains__['id']
    """
    # Need to re-import salt.config, somehow it got lost when a minion is starting
    import salt.config

    # if we have no grains, lets try loading from disk (TODO: move to decorator?)
    cfn = os.path.join(opts["cachedir"], "grains.cache.p")
    if not force_refresh and opts.get("grains_cache", False):
        cached_grains = _load_cached_grains(opts, cfn)
        if cached_grains:
            return cached_grains
    else:
        log.debug("Grains refresh requested. Refreshing grains.")

    if opts.get("skip_grains", False):
        return {}
    grains_deep_merge = opts.get("grains_deep_merge", False) is True
    if "conf_file" in opts:
        pre_opts = {}
        pre_opts.update(
            salt.config.load_config(
                opts["conf_file"],
                "SALT_MINION_CONFIG",
                salt.config.DEFAULT_MINION_OPTS["conf_file"],
            )
        )
        default_include = pre_opts.get("default_include", opts["default_include"])
        include = pre_opts.get("include", [])
        pre_opts.update(
            salt.config.include_config(
                default_include, opts["conf_file"], verbose=False
            )
        )
        pre_opts.update(
            salt.config.include_config(include, opts["conf_file"], verbose=True)
        )
        if "grains" in pre_opts:
            opts["grains"] = pre_opts["grains"]
        else:
            opts["grains"] = {}
    else:
        opts["grains"] = {}

    grains_data = {}
    blist = opts.get("grains_blacklist", [])
    funcs = grain_funcs(
        opts, proxy=proxy, context=context or {}, loaded_base_name=loaded_base_name
    )
    if force_refresh:  # if we refresh, lets reload grain modules
        funcs.clear()
    # Run core grains
    for key in funcs:
        if not key.startswith("core."):
            continue
        log.trace("Loading %s grain", key)
        ret = funcs[key]()
        if not isinstance(ret, dict):
            continue
        if blist:
            for key in list(ret):
                for block in blist:
                    if salt.utils.stringutils.expr_match(key, block):
                        del ret[key]
                        log.trace("Filtering %s grain", key)
            if not ret:
                continue
        if grains_deep_merge:
            salt.utils.dictupdate.update(grains_data, ret)
        else:
            grains_data.update(ret)

    # Run the rest of the grains
    for key in funcs:
        if key.startswith("core.") or key == "_errors":
            continue
        try:
            # Grains are loaded too early to take advantage of the injected
            # __proxy__ variable.  Pass an instance of that LazyLoader
            # here instead to grains functions if the grains functions take
            # one parameter.  Then the grains can have access to the
            # proxymodule for retrieving information from the connected
            # device.
            log.trace("Loading %s grain", key)
            parameters = inspect.signature(funcs[key]).parameters
            kwargs = {}
            if "proxy" in parameters:
                kwargs["proxy"] = proxy
            if "grains" in parameters:
                kwargs["grains"] = grains_data
            ret = funcs[key](**kwargs)
        except Exception:  # pylint: disable=broad-except
            if salt.utils.platform.is_proxy():
                log.info(
                    "The following CRITICAL message may not be an error; the proxy may not be completely established yet."
                )
            log.critical(
                "Failed to load grains defined in grain file %s in "
                "function %s, error:\n",
                key,
                funcs[key],
                exc_info=True,
            )
            continue
        if not isinstance(ret, dict):
            continue
        if blist:
            for key in list(ret):
                for block in blist:
                    if salt.utils.stringutils.expr_match(key, block):
                        del ret[key]
                        log.trace("Filtering %s grain", key)
            if not ret:
                continue
        if grains_deep_merge:
            salt.utils.dictupdate.update(grains_data, ret)
        else:
            grains_data.update(ret)

    if opts.get("proxy_merge_grains_in_module", True) and proxy:
        try:
            proxytype = proxy.opts["proxy"]["proxytype"]
            if proxytype + ".grains" in proxy:
                if (
                    proxytype + ".initialized" in proxy
                    and proxy[proxytype + ".initialized"]()
                ):
                    try:
                        proxytype = proxy.opts["proxy"]["proxytype"]
                        ret = proxy[proxytype + ".grains"]()
                        if grains_deep_merge:
                            salt.utils.dictupdate.update(grains_data, ret)
                        else:
                            grains_data.update(ret)
                    except Exception:  # pylint: disable=broad-except
                        log.critical(
                            "Failed to run proxy's grains function!", exc_info=True
                        )
        except KeyError:
            pass

    grains_data.update(opts["grains"])
    # Write cache if enabled
    if opts.get("grains_cache", False):
        with salt.utils.files.set_umask(0o077):
            try:
                if salt.utils.platform.is_windows():
                    # Late import
                    import salt.modules.cmdmod

                    # Make sure cache file isn't read-only
                    salt.modules.cmdmod._run_quiet(f'attrib -R "{cfn}"')
                with salt.utils.files.fopen(cfn, "w+b") as fp_:
                    try:
                        salt.payload.dump(grains_data, fp_)
                    except TypeError as e:
                        log.error("Failed to serialize grains cache: %s", e)
                        raise  # re-throw for cleanup
            except Exception as e:  # pylint: disable=broad-except
                log.error("Unable to write to grains cache file %s: %s", cfn, e)
                # Based on the original exception, the file may or may not have been
                # created. If it was, we will remove it now, as the exception means
                # the serialized data is not to be trusted, no matter what the
                # exception is.
                if os.path.isfile(cfn):
                    os.unlink(cfn)

    if grains_deep_merge:
        salt.utils.dictupdate.update(grains_data, opts["grains"])
    else:
        grains_data.update(opts["grains"])
    return salt.utils.data.decode(grains_data, preserve_tuples=True)


# TODO: get rid of? Does anyone use this? You should use raw() instead
def call(fun, **kwargs):
    """
    Directly call a function inside a loader directory
    """
    args = kwargs.get("args", [])
    dirs = kwargs.get("dirs", [])
    loaded_base_name = kwargs.pop("loaded_base_name", None)

    funcs = LazyLoader(
        [str(SALT_BASE_PATH / "modules")] + dirs,
        None,
        tag="modules",
        virtual_enable=False,
        loaded_base_name=loaded_base_name,
    )
    return funcs[fun](*args)


def runner(opts, utils=None, context=None, whitelist=None, loaded_base_name=None):
    """
    Directly call a function inside a loader directory

    :param dict opts: The Salt options dictionary
    :param list whitelist: A list of modules which should be whitelisted.
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param LazyLoader utils: A LazyLoader instance returned from ``utils``.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    if utils is None:
        utils = {}
    if context is None:
        context = {}
    return LazyLoader(
        _module_dirs(opts, "runners", "runner", ext_type_dirs="runner_dirs"),
        opts,
        tag="runners",
        pack={"__utils__": utils, "__context__": context},
        whitelist=whitelist,
        extra_module_dirs=utils.module_dirs if utils else None,
        # TODO: change from __salt__ to something else, we overload __salt__ too much
        pack_self="__salt__",
        loaded_base_name=loaded_base_name,
    )


def queues(opts, loaded_base_name=None):
    """
    Directly call a function inside a loader directory

    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(opts, "queues", "queue", ext_type_dirs="queue_dirs"),
        opts,
        tag="queues",
        loaded_base_name=loaded_base_name,
    )


def sdb(opts, functions=None, whitelist=None, utils=None, loaded_base_name=None):
    """
    Make a very small database call

    :param dict opts: The Salt options dictionary
    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param list whitelist: A list of modules which should be whitelisted.
    :param LazyLoader utils: A LazyLoader instance returned from ``utils``.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    if utils is None:
        utils = {}

    return LazyLoader(
        _module_dirs(opts, "sdb"),
        opts,
        tag="sdb",
        pack={
            "__sdb__": functions,
            "__utils__": utils,
            "__salt__": minion_mods(opts, utils=utils),
        },
        whitelist=whitelist,
        extra_module_dirs=utils.module_dirs if utils else None,
        loaded_base_name=loaded_base_name,
    )


def pkgdb(opts, loaded_base_name=None):
    """
    Return modules for SPM's package database

    .. versionadded:: 2015.8.0

    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(opts, "pkgdb", base_path=str(SALT_BASE_PATH / "spm")),
        opts,
        tag="pkgdb",
        loaded_base_name=loaded_base_name,
    )


def pkgfiles(opts, loaded_base_name=None):
    """
    Return modules for SPM's file handling

    .. versionadded:: 2015.8.0


    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(opts, "pkgfiles", base_path=str(SALT_BASE_PATH / "spm")),
        opts,
        tag="pkgfiles",
        loaded_base_name=loaded_base_name,
    )


def clouds(opts, loaded_base_name=None):
    """
    Return the cloud functions

    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    _utils = utils(opts)
    # Let's bring __active_provider_name__, defaulting to None, to all cloud
    # drivers. This will get temporarily updated/overridden with a context
    # manager when needed.
    functions = LazyLoader(
        _module_dirs(
            opts,
            "clouds",
            "cloud",
            base_path=str(SALT_BASE_PATH / "cloud"),
            int_type="clouds",
        ),
        opts,
        tag="clouds",
        pack={"__utils__": _utils, "__active_provider_name__": None},
        extra_module_dirs=_utils.module_dirs,
        loaded_base_name=loaded_base_name,
    )
    for funcname in LIBCLOUD_FUNCS_NOT_SUPPORTED:
        log.trace(
            "'%s' has been marked as not supported. Removing from the "
            "list of supported cloud functions",
            funcname,
        )
        functions.pop(funcname, None)
    return functions


def netapi(opts, loaded_base_name=None):
    """
    Return the network api functions

    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(opts, "netapi"),
        opts,
        tag="netapi",
        loaded_base_name=loaded_base_name,
    )


def executors(opts, functions=None, context=None, proxy=None, loaded_base_name=None):
    """
    Returns the executor modules

    :param dict opts: The Salt options dictionary
    :param LazyLoader functions: A LazyLoader instance returned from ``minion_mods``.
    :param dict context: A Salt context that should be made present inside
                            generated modules in __context__
    :param LazyLoader proxy: An optional LazyLoader instance returned from ``proxy``.
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    if proxy is None:
        proxy = {}
    if context is None:
        context = {}
    return LazyLoader(
        _module_dirs(opts, "executors", "executor"),
        opts,
        tag="executor",
        pack={"__salt__": functions, "__context__": context, "__proxy__": proxy},
        pack_self="__executors__",
        loaded_base_name=loaded_base_name,
    )


def cache(opts, loaded_base_name=None):
    """
    Returns the returner modules

    :param dict opts: The Salt options dictionary
    :param str loaded_base_name: The imported modules namespace when imported
                                 by the salt loader.
    """
    return LazyLoader(
        _module_dirs(opts, "cache", "cache"),
        opts,
        tag="cache",
        loaded_base_name=loaded_base_name,
    )


@contextlib.contextmanager
def catch_entry_points_exception(entry_point):
    context = types.SimpleNamespace(exception_caught=False)
    try:
        yield context
    except Exception as exc:  # pylint: disable=broad-except
        context.exception_caught = True
        entry_point_details = entrypoints.name_and_version_from_entry_point(entry_point)
        log.error(
            "Error processing Salt Extension %s(version: %s): %s",
            entry_point_details.name,
            entry_point_details.version,
            exc,
            exc_info_on_loglevel=logging.DEBUG,
        )
