"""
Use secrets sourced from Vault (or OpenBao) in minion pillars.

.. important::
    This module requires the general :ref:`Vault setup <vault-setup>`.

.. warning::
    A minion must not be able to write to its own pillar source path,
    otherwise a core security assumption in Salt is violated.

.. versionchanged:: 1.0.0
    Previous versions of this pillar module found in Salt core were configured
    with a parameter named ``conf``, expecting a single value representing
    the path to include in the pillar with the prefix ``path=``.
    This parameter has been deprecated. Please configure this pillar module
    either by just passing the path or declaring it as ``path: <path>``.

Setup
-----
Include this module in your :conf_master:`ext_pillar` configuration:

.. code-block:: yaml

    ext_pillar:
      - vault: salt/global

.. hint::
    You can also include multiple instances of this module in your configuration.

Now all keys of the Vault KV path ``salt/global`` are inserted into each
minion's pillar, which is quite inflexible and usually not what is wanted.
To work around that, you can :ref:`template the path <vault-templating>`.

.. code-block:: yaml

    ext_pillar:
      - vault: salt/minions/{minion}
      - vault: salt/roles/{pillar[roles]}

.. note::
    There is currently no ``top.sls`` equivalent.

.. note::
    If a pattern matches multiple paths, the results are merged according
    to the master configuration values :conf_master:`pillar_source_merging_strategy <pillar_source_merging_strategy>`
    and :conf_master:`pillar_merge_lists <pillar_merge_lists>` by default.
    If the optional :vconf:`nesting_key <pillar.nesting_key>` is defined,
    the merged result is nested below.
    There is currently no way to nest multiple results under different keys.

.. vconf:: pillar

Configuration reference
-----------------------
.. vconf:: pillar.path

``path``
    The path to include in the minion pillars. Can be :ref:`templated <vault-templating>`.

.. vconf:: pillar.nesting_key

``nesting_key``
    The Vault-sourced pillar values are usually merged into the root
    of the pillar. This option allows you to specify a parent key
    under which all values are nested. If the key contains previous
    values, they are merged.

.. vconf:: pillar.merge_strategy

``merge_strategy``
    When multiple paths are matched by a templated path, use this merge strategy
    instead of :conf_master:`pillar_source_merging_strategy <pillar_source_merging_strategy>`.

.. vconf:: pillar.merge_lists

``merge_lists``
    Override the default set by :conf_master:`pillar_merge_lists <pillar_merge_lists>`.


Complete configuration
----------------------
.. code-block:: yaml

    ext_pillar:
      - vault:
           path: salt/roles/{pillar[roles]}
           nesting_key: vault_sourced
           merge_strategy: smart
           merge_lists: false
"""

import logging

import salt.utils.dictupdate
from salt.exceptions import InvalidConfigError
from salt.exceptions import SaltException

from saltext.vault.utils import vault
from saltext.vault.utils.vault import helpers
from saltext.vault.utils.versions import warn_until

log = logging.getLogger(__name__)


def ext_pillar(
    minion_id,  # pylint: disable=W0613
    pillar,  # pylint: disable=W0613
    path=None,
    nesting_key=None,
    merge_strategy=None,
    merge_lists=None,
    extra_minion_data=None,
    conf=None,
):
    """
    Get pillar data from Vault for the configuration ``conf``.
    """
    extra_minion_data = extra_minion_data or {}
    if extra_minion_data.get("_vault_runner_is_compiling_pillar_templates"):
        # Disable vault ext_pillar while compiling pillar for vault policy templates
        return {}
    if conf is not None:
        comps = conf.split()
        paths = [comp for comp in comps if comp.startswith("path=")]
        if not paths:
            log.error('"%s" is not a valid Vault ext_pillar config', conf)
            return {}
        path_pattern = paths[0].replace("path=", "")
        warn_until(
            2,
            (
                "The `conf` parameter to the Vault pillar is deprecated. "
                "Please migrate to the `path` parameter. It takes the path "
                "as its parameter, without the `path=` prefix."
            ),
        )
    elif path is not None:
        comps = path.split()
        if not comps:
            log.error('"%s" is not a valid Vault ext_pillar config', path)
            return {}
        if len(comps) > 1:
            warn_until(
                2,
                (
                    "The `conf` parameter to the Vault pillar is deprecated. "
                    "Please migrate to the `path` parameter. It takes the path "
                    "as its parameter, without the `path=` prefix."
                ),
            )
            comps = [comp for comp in comps if comp.startswith("path=")]
            if not comps:
                log.error('"%s" is not a valid Vault ext_pillar config', path)
                return {}
        path = comps[0]
        if path.startswith("path="):
            warn_until(
                2,
                (
                    "The Vault pillar module should not be configured with "
                    "the `path=` prefix anymore. Please remove it from your "
                    "configuration."
                ),
            )
            path = path[5:]
        path_pattern = path
    else:
        raise InvalidConfigError("Need a Vault path to include in the pillar")

    merge_strategy = merge_strategy or __opts__.get("pillar_source_merging_strategy", "smart")
    merge_lists = merge_lists or __opts__.get("pillar_merge_lists", False)

    vault_pillar = {}

    for rendered_path in _get_paths(path_pattern, minion_id, pillar):
        try:
            vault_pillar_single = vault.read_kv(rendered_path, __opts__, __context__)
        except vault.VaultNotFoundError:
            log.info(
                "Vault secret not found for: %s", rendered_path, exc_info_on_loglevel=logging.DEBUG
            )
        except SaltException:
            log.warning(
                "Error fetching Vault secret at: %s",
                rendered_path,
                exc_info_on_loglevel=logging.DEBUG,
            )
        else:
            vault_pillar = salt.utils.dictupdate.merge(
                vault_pillar,
                vault_pillar_single,
                strategy=merge_strategy,
                merge_lists=merge_lists,
            )

    if nesting_key:
        vault_pillar = {nesting_key: vault_pillar}
    return vault_pillar


def _get_paths(path_pattern, minion_id, pillar):
    """
    Get the paths that should be merged into the pillar dict
    """
    mappings = {"minion": minion_id, "pillar": pillar}

    paths = []
    try:
        for expanded_pattern in helpers.expand_pattern_lists(path_pattern, **mappings):
            paths.append(expanded_pattern.format(**mappings))
    except KeyError:
        log.warning("Could not resolve pillar path pattern %s", path_pattern)

    log.debug("%s vault pillar paths: %s", minion_id, paths)
    return paths
