"""
The match module allows for match routines to be run and determine target specs
"""

import copy
import inspect
import logging
import sys
from collections.abc import Mapping

import salt.loader
from salt.defaults import DEFAULT_TARGET_DELIM
from salt.exceptions import SaltException

__func_alias__ = {"list_": "list"}

log = logging.getLogger(__name__)


def compound(tgt, minion_id=None):
    """
    Return True if the minion ID matches the given compound target

    minion_id
        Specify the minion ID to match against the target expression

        .. versionadded:: 2014.7.0

    CLI Example:

    .. code-block:: bash

        salt '*' match.compound 'L@cheese,foo and *'
    """
    if minion_id is not None:
        if not isinstance(minion_id, str):
            minion_id = str(minion_id)
    matchers = salt.loader.matchers(__opts__)
    try:
        ret = matchers["compound_match.match"](tgt, opts=__opts__, minion_id=minion_id)
    except Exception as exc:  # pylint: disable=broad-except
        log.exception(exc)
        ret = False

    return ret


def ipcidr(tgt):
    """
    Return True if the minion matches the given ipcidr target

    CLI Example:

    .. code-block:: bash

        salt '*' match.ipcidr '192.168.44.0/24'

    delimiter
    Pillar Example:

    .. code-block:: yaml

       '172.16.0.0/12':
         - match: ipcidr
         - nodeclass: internal

    """
    matchers = salt.loader.matchers(__opts__)
    try:
        return matchers["ipcidr_match.match"](tgt, opts=__opts__)
    except Exception as exc:  # pylint: disable=broad-except
        log.exception(exc)
        return False


def pillar_pcre(tgt, delimiter=DEFAULT_TARGET_DELIM):
    """
    Return True if the minion matches the given pillar_pcre target. The
    ``delimiter`` argument can be used to specify a different delimiter.

    CLI Example:

    .. code-block:: bash

        salt '*' match.pillar_pcre 'cheese:(swiss|american)'
        salt '*' match.pillar_pcre 'clone_url|https://github\\.com/.*\\.git' delimiter='|'

    delimiter
        Specify an alternate delimiter to use when traversing a nested dict

        .. versionadded:: 2014.7.0

    delim
        Specify an alternate delimiter to use when traversing a nested dict

        .. versionadded:: 0.16.4
        .. deprecated:: 2015.8.0
    """
    matchers = salt.loader.matchers(__opts__)
    try:
        return matchers["pillar_pcre_match.match"](
            tgt, delimiter=delimiter, opts=__opts__
        )
    except Exception as exc:  # pylint: disable=broad-except
        log.exception(exc)
        return False


def pillar(tgt, delimiter=DEFAULT_TARGET_DELIM):
    """
    Return True if the minion matches the given pillar target. The
    ``delimiter`` argument can be used to specify a different delimiter.

    CLI Example:

    .. code-block:: bash

        salt '*' match.pillar 'cheese:foo'
        salt '*' match.pillar 'clone_url|https://github.com/saltstack/salt.git' delimiter='|'

    delimiter
        Specify an alternate delimiter to use when traversing a nested dict

        .. versionadded:: 2014.7.0

    delim
        Specify an alternate delimiter to use when traversing a nested dict

        .. versionadded:: 0.16.4
        .. deprecated:: 2015.8.0
    """
    matchers = salt.loader.matchers(__opts__)
    try:
        return matchers["pillar_match.match"](tgt, delimiter=delimiter, opts=__opts__)
    except Exception as exc:  # pylint: disable=broad-except
        log.exception(exc)
        return False


def data(tgt):
    """
    Return True if the minion matches the given data target

    CLI Example:

    .. code-block:: bash

        salt '*' match.data 'spam:eggs'
    """
    matchers = salt.loader.matchers(__opts__)
    try:
        return matchers["data_match.match"](tgt, opts=__opts__)
    except Exception as exc:  # pylint: disable=broad-except
        log.exception(exc)
        return False


def grain_pcre(tgt, delimiter=DEFAULT_TARGET_DELIM):
    """
    Return True if the minion matches the given grain_pcre target. The
    ``delimiter`` argument can be used to specify a different delimiter.

    CLI Example:

    .. code-block:: bash

        salt '*' match.grain_pcre 'os:Fedo.*'
        salt '*' match.grain_pcre 'ipv6|2001:.*' delimiter='|'

    delimiter
        Specify an alternate delimiter to use when traversing a nested dict

        .. versionadded:: 2014.7.0

    delim
        Specify an alternate delimiter to use when traversing a nested dict

        .. versionadded:: 0.16.4
        .. deprecated:: 2015.8.0
    """
    matchers = salt.loader.matchers(__opts__)
    try:
        return matchers["grain_pcre_match.match"](
            tgt, delimiter=delimiter, opts=__opts__
        )
    except Exception as exc:  # pylint: disable=broad-except
        log.exception(exc)
        return False


def grain(tgt, delimiter=DEFAULT_TARGET_DELIM):
    """
    Return True if the minion matches the given grain target. The ``delimiter``
    argument can be used to specify a different delimiter.

    CLI Example:

    .. code-block:: bash

        salt '*' match.grain 'os:Ubuntu'
        salt '*' match.grain 'ipv6|2001:db8::ff00:42:8329' delimiter='|'

    delimiter
        Specify an alternate delimiter to use when traversing a nested dict

        .. versionadded:: 2014.7.0

    delim
        Specify an alternate delimiter to use when traversing a nested dict

        .. versionadded:: 0.16.4
        .. deprecated:: 2015.8.0
    """
    matchers = salt.loader.matchers(__opts__)
    try:
        return matchers["grain_match.match"](tgt, delimiter=delimiter, opts=__opts__)
    except Exception as exc:  # pylint: disable=broad-except
        log.exception(exc)
        return False


def list_(tgt, minion_id=None):
    """
    Return True if the minion ID matches the given list target

    minion_id
        Specify the minion ID to match against the target expression

        .. versionadded:: 2014.7.0

    CLI Example:

    .. code-block:: bash

        salt '*' match.list 'server1,server2'
    """
    if minion_id is not None:
        if not isinstance(minion_id, str):
            minion_id = str(minion_id)
    matchers = salt.loader.matchers(__opts__)
    try:
        return matchers["list_match.match"](tgt, opts=__opts__, minion_id=minion_id)
    except Exception as exc:  # pylint: disable=broad-except
        log.exception(exc)
        return False


def pcre(tgt, minion_id=None):
    """
    Return True if the minion ID matches the given pcre target

    minion_id
        Specify the minion ID to match against the target expression

        .. versionadded:: 2014.7.0

    CLI Example:

    .. code-block:: bash

        salt '*' match.pcre '.*'
    """
    if minion_id is not None:
        if not isinstance(minion_id, str):
            minion_id = str(minion_id)
    matchers = salt.loader.matchers(__opts__)
    try:
        return matchers["pcre_match.match"](tgt, opts=__opts__, minion_id=minion_id)
    except Exception as exc:  # pylint: disable=broad-except
        log.exception(exc)
        return False


def glob(tgt, minion_id=None):
    """
    Return True if the minion ID matches the given glob target

    minion_id
        Specify the minion ID to match against the target expression

        .. versionadded:: 2014.7.0

    CLI Example:

    .. code-block:: bash

        salt '*' match.glob '*'
    """
    if minion_id is not None:
        if not isinstance(minion_id, str):
            minion_id = str(minion_id)
    matchers = salt.loader.matchers(__opts__)

    try:
        return matchers["glob_match.match"](tgt, opts=__opts__, minion_id=minion_id)
    except Exception as exc:  # pylint: disable=broad-except
        log.exception(exc)
        return False


def filter_by(
    lookup,
    tgt_type="compound",
    minion_id=None,
    merge=None,
    merge_lists=False,
    default="default",
):
    """
    Return the first match in a dictionary of target patterns

    .. versionadded:: 2014.7.0

    CLI Example:

    .. code-block:: bash

        salt '*' match.filter_by '{foo*: Foo!, bar*: Bar!}' minion_id=bar03

    Pillar Example:

    .. code-block:: jinja

        # Filter the data for the current minion into a variable:
        {% set roles = salt['match.filter_by']({
            'web*': ['app', 'caching'],
            'db*': ['db'],
        }, minion_id=grains['id'], default='web*') %}

        # Make the filtered data available to Pillar:
        roles: {{ roles | yaml() }}
    """
    expr_funcs = dict(
        inspect.getmembers(sys.modules[__name__], predicate=inspect.isfunction)
    )

    for key in lookup:
        params = (key, minion_id) if minion_id else (key,)
        if expr_funcs[tgt_type](*params):
            if merge:
                if not isinstance(merge, Mapping):
                    raise SaltException(
                        "filter_by merge argument must be a dictionary."
                    )

                if lookup[key] is None:
                    return merge
                else:
                    salt.utils.dictupdate.update(
                        lookup[key], copy.deepcopy(merge), merge_lists=merge_lists
                    )

            return lookup[key]

    return lookup.get(default, None)


def search_by(lookup, tgt_type="compound", minion_id=None):
    """
    Search a dictionary of target strings for matching targets

    This is the inverse of :py:func:`match.filter_by
    <salt.modules.match.filter_by>` and allows matching values instead of
    matching keys. A minion can be matched by multiple entries.

    .. versionadded:: 2017.7.0

    CLI Example:

    .. code-block:: bash

        salt '*' match.search_by '{web: [node1, node2], db: [node2, node]}'

    Pillar Example:

    .. code-block:: jinja

        {% set roles = salt.match.search_by({
            'web': ['G@os_family:Debian not nodeX'],
            'db': ['L@node2,node3 and G@datacenter:west'],
            'caching': ['node3', 'node4'],
        }) %}

        # Make the filtered data available to Pillar:
        roles: {{ roles | yaml() }}
    """
    expr_funcs = dict(
        inspect.getmembers(sys.modules[__name__], predicate=inspect.isfunction)
    )

    matches = []
    for key, target_list in lookup.items():
        for target in target_list:
            params = (target, minion_id) if minion_id else (target,)
            if expr_funcs[tgt_type](*params):
                matches.append(key)

    return matches or None
