"""
Module to work with salt formula defaults files

"""


import copy
import logging
import os

import salt.fileclient
import salt.utils.data
import salt.utils.dictupdate as dictupdate
import salt.utils.files
import salt.utils.json
import salt.utils.url
import salt.utils.yaml

__virtualname__ = "defaults"

log = logging.getLogger(__name__)


def _mk_client():
    """
    Create a file client and add it to the context
    """
    if "cp.fileclient" not in __context__:
        __context__["cp.fileclient"] = salt.fileclient.get_file_client(__opts__)


def _load(formula):
    """
    Generates a list of salt://<formula>/defaults.(json|yaml) files
    and fetches them from the Salt master.

    Returns first defaults file as python dict.
    """

    # Compute possibilities
    _mk_client()
    paths = []
    for ext in ("yaml", "json"):
        source_url = salt.utils.url.create(formula + "/defaults." + ext)
        paths.append(source_url)
    # Fetch files from master
    defaults_files = __context__["cp.fileclient"].cache_files(paths)

    for file_ in defaults_files:
        if not file_:
            # Skip empty string returned by cp.fileclient.cache_files.
            continue

        suffix = file_.rsplit(".", 1)[-1]
        if suffix == "yaml":
            loader = salt.utils.yaml.safe_load
        elif suffix == "json":
            loader = salt.utils.json.load
        else:
            log.debug("Failed to determine loader for %r", file_)
            continue

        if os.path.exists(file_):
            log.debug("Reading defaults from %r", file_)
            with salt.utils.files.fopen(file_) as fhr:
                defaults = loader(fhr)
                log.debug("Read defaults %r", defaults)

            return defaults or {}


def get(key, default=""):
    """
    defaults.get is used much like pillar.get except that it will read
    a default value for a pillar from defaults.json or defaults.yaml
    files that are stored in the root of a salt formula.

    CLI Example:

    .. code-block:: bash

        salt '*' defaults.get core:users:root

    The defaults is computed from pillar key. The first entry is considered as
    the formula namespace.

    For example, querying ``core:users:root`` will try to load
    ``salt://core/defaults.yaml`` and ``salt://core/defaults.json``.
    """

    # Determine formula namespace from query
    if ":" in key:
        namespace, key = key.split(":", 1)
    else:
        namespace, key = key, None

    # Fetch and load defaults formula files from states.
    defaults = _load(namespace)

    # Fetch value
    if key:
        return salt.utils.data.traverse_dict_and_list(defaults, key, default)
    else:
        return defaults


def merge(dest, src, merge_lists=False, in_place=True):
    """
    defaults.merge
        Allows deep merging of dicts in formulas.

    merge_lists : False
        If True, it will also merge lists instead of replace their items.

    in_place : True
        If True, it will merge into dest dict,
        if not it will make a new copy from that dict and return it.

    CLI Example:

    .. code-block:: bash

        salt '*' defaults.merge '{a: b}' '{d: e}'

    It is more typical to use this in a templating language in formulas,
    instead of directly on the command-line.
    """
    if in_place:
        merged = dest
    else:
        merged = copy.deepcopy(dest)
    return dictupdate.update(merged, src, merge_lists=merge_lists)


def deepcopy(source):
    """
    defaults.deepcopy
        Allows deep copy of objects in formulas.

        By default, Python does not copy objects,
        it creates bindings between a target and an object.

    It is more typical to use this in a templating language in formulas,
    instead of directly on the command-line.
    """
    return copy.deepcopy(source)


def update(dest, defaults, merge_lists=True, in_place=True):
    """
    defaults.update
        Allows setting defaults for group of data set e.g. group for nodes.

        This function is a combination of defaults.merge
        and defaults.deepcopy to avoid redundant in jinja.

        Example:

        .. code-block:: yaml

            group01:
              defaults:
                enabled: True
                extra:
                  - test
                  - stage
              nodes:
                host01:
                  index: foo
                  upstream: bar
                host02:
                  index: foo2
                  upstream: bar2

        .. code-block:: jinja

            {% do salt['defaults.update'](group01.nodes, group01.defaults) %}

        Each node will look like the following:

        .. code-block:: yaml

            host01:
              enabled: True
              index: foo
              upstream: bar
              extra:
                - test
                - stage

    merge_lists : True
        If True, it will also merge lists instead of replace their items.

    in_place : True
        If True, it will merge into dest dict.
        if not it will make a new copy from that dict and return it.

    It is more typical to use this in a templating language in formulas,
    instead of directly on the command-line.
    """

    if in_place:
        nodes = dest
    else:
        nodes = deepcopy(dest)

    for node_name, node_vars in nodes.items():
        defaults_vars = deepcopy(defaults)
        node_vars = merge(defaults_vars, node_vars, merge_lists=merge_lists)
        nodes[node_name] = node_vars

    return nodes
