# -*- coding: utf-8 -*-
'''
.. versionadded:: 2017.7

Management of Zabbix Template object over Zabbix API.

:codeauthor: Jakub Sliva <jakub.sliva@ultimum.io>
'''
from __future__ import absolute_import
from __future__ import unicode_literals
import logging
import json

try:
    from salt.ext import six
    from salt.exceptions import SaltException
    IMPORTS_OK = True
except ImportError:
    IMPORTS_OK = False


log = logging.getLogger(__name__)

TEMPLATE_RELATIONS = ['groups', 'hosts', 'macros']
TEMPLATE_COMPONENT_ORDER = ('applications',
                            'items',
                            'gitems',
                            'graphs',
                            'screens',
                            'httpTests',
                            'triggers',
                            'discoveries')
DISCOVERYRULE_COMPONENT_ORDER = ('itemprototypes', 'triggerprototypes', 'graphprototypes', 'hostprototypes')
TEMPLATE_COMPONENT_DEF = {
    # 'component': {'qtype':        'component type to query',
    #               'qidname':      'component id name',
    #               'qselectpid':   'particular component selection attribute name (parent id name)',
    #               'ptype':        'parent component type',
    #               'pid':          'parent component id',
    #               'pid_ref_name': 'component's creation reference name for parent id',
    #               'res_id_name':  'jsonrpc modification call result key name of list of affected IDs'},
    #               'output':       {'output': 'extend', 'selectApplications': 'extend', 'templated': 'true'},
    #               'inherited':    'attribute name for inheritance toggling',
    #               'filter':       'child component unique identification attribute name',
    'applications': {'qtype': 'application',
                     'qidname': 'applicationid',
                     'qselectpid': 'templateids',
                     'ptype': 'template',
                     'pid': 'templateid',
                     'pid_ref_name': 'hostid',
                     'res_id_name': 'applicationids',
                     'output': {'output': 'extend', 'templated': 'true'},
                     'inherited': 'inherited',
                     'adjust': True,
                     'filter': 'name',
                     'ro_attrs': ['applicationid', 'flags', 'templateids']},
    'items': {'qtype': 'item',
              'qidname': 'itemid',
              'qselectpid': 'templateids',
              'ptype': 'template',
              'pid': 'templateid',
              'pid_ref_name': 'hostid',
              'res_id_name': 'itemids',
              'output': {'output': 'extend', 'selectApplications': 'extend', 'templated': 'true'},
              'inherited': 'inherited',
              'adjust': False,
              'filter': 'name',
              'ro_attrs': ['itemid', 'error', 'flags', 'lastclock', 'lastns',
                           'lastvalue', 'prevvalue', 'state', 'templateid']},
    'triggers': {'qtype': 'trigger',
                 'qidname': 'triggerid',
                 'qselectpid': 'templateids',
                 'ptype': 'template',
                 'pid': 'templateid',
                 'pid_ref_name': None,
                 'res_id_name': 'triggerids',
                 'output': {'output': 'extend', 'selectDependencies': 'expand',
                            'templated': 'true', 'expandExpression': 'true'},
                 'inherited': 'inherited',
                 'adjust': False,
                 'filter': 'description',
                 'ro_attrs': ['error', 'flags', 'lastchange', 'state', 'templateid', 'value']},
    'graphs': {'qtype': 'graph',
               'qidname': 'graphid',
               'qselectpid': 'templateids',
               'ptype': 'template',
               'pid': 'templateid',
               'pid_ref_name': None,
               'res_id_name': 'graphids',
               'output': {'output': 'extend', 'selectGraphItems': 'extend', 'templated': 'true'},
               'inherited': 'inherited',
               'adjust': False,
               'filter': 'name',
               'ro_attrs': ['graphid', 'flags', 'templateid']},
    'gitems': {'qtype': 'graphitem',
               'qidname': 'itemid',
               'qselectpid': 'graphids',
               'ptype': 'graph',
               'pid': 'graphid',
               'pid_ref_name': None,
               'res_id_name': None,
               'output': {'output': 'extend'},
               'inherited': 'inherited',
               'adjust': False,
               'filter': 'name',
               'ro_attrs': ['gitemid']},
    # "Template screen"
    'screens': {'qtype': 'templatescreen',
                'qidname': 'screenid',
                'qselectpid': 'templateids',
                'ptype': 'template',
                'pid': 'templateid',
                'pid_ref_name': 'templateid',
                'res_id_name': 'screenids',
                'output': {'output': 'extend', 'selectUsers': 'extend', 'selectUserGroups': 'extend',
                           'selectScreenItems': 'extend', 'noInheritance': 'true'},
                'inherited': 'noInheritance',
                'adjust': False,
                'filter': 'name',
                'ro_attrs': ['screenid']},
    # "LLD rule"
    'discoveries': {'qtype': 'discoveryrule',
                    'qidname': 'itemid',
                    'qselectpid': 'templateids',
                    'ptype': 'template',
                    'pid': 'templateid',
                    'pid_ref_name': 'hostid',
                    'res_id_name': 'itemids',
                    'output': {'output': 'extend', 'selectFilter': 'extend', 'templated': 'true'},
                    'inherited': 'inherited',
                    'adjust': False,
                    'filter': 'key_',
                    'ro_attrs': ['itemid', 'error', 'state', 'templateid']},
    # "Web scenario"
    'httpTests': {'qtype': 'httptest',
                  'qidname': 'httptestid',
                  'qselectpid': 'templateids',
                  'ptype': 'template',
                  'pid': 'templateid',
                  'pid_ref_name': 'hostid',
                  'res_id_name': 'httptestids',
                  'output': {'output': 'extend', 'selectSteps': 'extend', 'templated': 'true'},
                  'inherited': 'inherited',
                  'adjust': False,
                  'filter': 'name',
                  'ro_attrs': ['httptestid', 'nextcheck', 'templateid']},
    # discoveries => discoveryrule
    'itemprototypes': {'qtype': 'itemprototype',
                       'qidname': 'itemid',
                       'qselectpid': 'discoveryids',
                       'ptype': 'discoveryrule',
                       'pid': 'itemid',
                       'pid_ref_name': 'ruleid',
                       # exception only in case of itemprototype - needs both parent ruleid and hostid
                       'pid_ref_name2': 'hostid',
                       'res_id_name': 'itemids',
                       'output': {'output': 'extend', 'selectSteps': 'extend', 'selectApplications': 'extend',
                                  'templated': 'true'},
                       'adjust': False,
                       'inherited': 'inherited',
                       'filter': 'name',
                       'ro_attrs': ['itemid', 'templateid']},
    'triggerprototypes': {'qtype': 'triggerprototype',
                          'qidname': 'triggerid',
                          'qselectpid': 'discoveryids',
                          'ptype': 'discoveryrule',
                          'pid': 'itemid',
                          'pid_ref_name': None,
                          'res_id_name': 'triggerids',
                          'output': {'output': 'extend', 'selectTags': 'extend', 'selectDependencies': 'extend',
                                     'templated': 'true', 'expandExpression': 'true'},
                          'inherited': 'inherited',
                          'adjust': False,
                          'filter': 'description',
                          'ro_attrs': ['triggerid', 'templateid']},
    'graphprototypes': {'qtype': 'graphprototype',
                        'qidname': 'graphid',
                        'qselectpid': 'discoveryids',
                        'ptype': 'discoveryrule',
                        'pid': 'itemid',
                        'pid_ref_name': None,
                        'res_id_name': 'graphids',
                        'output': {'output': 'extend', 'selectGraphItems': 'extend', 'templated': 'true'},
                        'inherited': 'inherited',
                        'adjust': False,
                        'filter': 'name',
                        'ro_attrs': ['graphid', 'templateid']},
    'hostprototypes': {'qtype': 'hostprototype',
                       'qidname': 'hostid',
                       'qselectpid': 'discoveryids',
                       'ptype': 'discoveryrule',
                       'pid': 'itemid',
                       'pid_ref_name': 'ruleid',
                       'res_id_name': 'hostids',
                       'output': {'output': 'extend', 'selectGroupLinks': 'expand', 'selectGroupPrototypes': 'expand',
                                  'selectTemplates': 'expand'},
                       'inherited': 'inherited',
                       'adjust': False,
                       'filter': 'host',
                       'ro_attrs': ['hostid', 'templateid']}
}

# CHANGE_STACK = [{'component': 'items', 'action': 'create', 'params': dict|list}]
CHANGE_STACK = []


def __virtual__():
    '''
    Only make these states available if Zabbix module and run_query function is available
    and all 3rd party modules imported.
    '''
    if 'zabbix.run_query' in __salt__ and IMPORTS_OK:
        return True
    return False, 'Import zabbix or other needed modules failed.'


def _diff_and_merge_host_list(defined, existing):
    '''
    If Zabbix template is to be updated then list of assigned hosts must be provided in all or nothing manner to prevent
    some externally assigned hosts to be detached.

    :param defined: list of hosts defined in sls
    :param existing: list of hosts taken from live Zabbix
    :return: list to be updated (combinated or empty list)
    '''
    try:
        defined_host_ids = set([host['hostid'] for host in defined])
        existing_host_ids = set([host['hostid'] for host in existing])
    except KeyError:
        raise SaltException('List of hosts in template not defined correctly.')

    diff = defined_host_ids - existing_host_ids
    return [{'hostid': six.text_type(hostid)} for hostid in diff | existing_host_ids] if diff else []


def _get_existing_template_c_list(component, parent_id, **kwargs):
    '''
    Make a list of given component type not inherited from other templates because Zabbix API returns only list of all
    and list of inherited component items so we have to do a difference list.

    :param component: Template component (application, item, etc...)
    :param parent_id: ID of existing template the component is assigned to
    :return List of non-inherited (own) components
    '''
    c_def = TEMPLATE_COMPONENT_DEF[component]
    q_params = dict(c_def['output'])
    q_params.update({c_def['qselectpid']: parent_id})

    existing_clist_all = __salt__['zabbix.run_query'](c_def['qtype'] + '.get', q_params, **kwargs)

    # in some cases (e.g. templatescreens) the logic is reversed (even name of the flag is different!)
    if c_def['inherited'] == 'inherited':
        q_params.update({c_def['inherited']: 'true'})
        existing_clist_inherited = __salt__['zabbix.run_query'](c_def['qtype'] + '.get', q_params, **kwargs)
    else:
        existing_clist_inherited = []

    if existing_clist_inherited:
        return [c_all for c_all in existing_clist_all if c_all not in existing_clist_inherited]

    return existing_clist_all


def _adjust_object_lists(obj):
    '''
    For creation or update of object that have attribute which contains a list Zabbix awaits plain list of IDs while
    querying Zabbix for same object returns list of dicts

    :param obj: Zabbix object parameters
    '''
    for subcomp in TEMPLATE_COMPONENT_DEF:
        if subcomp in obj and TEMPLATE_COMPONENT_DEF[subcomp]['adjust']:
            obj[subcomp] = [item[TEMPLATE_COMPONENT_DEF[subcomp]['qidname']] for item in obj[subcomp]]


def _manage_component(component, parent_id, defined, existing, template_id=None, **kwargs):
    '''
    Takes particular component list, compares it with existing, call appropriate API methods - create, update, delete.

    :param component: component name
    :param parent_id: ID of parent entity under which component should be created
    :param defined: list of defined items of named component
    :param existing: list of existing items of named component
    :param template_id: In case that component need also template ID for creation (although parent_id is given?!?!?)
    '''
    zabbix_id_mapper = __salt__['zabbix.get_zabbix_id_mapper']()

    dry_run = __opts__['test']
    c_def = TEMPLATE_COMPONENT_DEF[component]
    compare_key = c_def['filter']

    defined_set = set([item[compare_key] for item in defined])
    existing_set = set([item[compare_key] for item in existing])

    create_set = defined_set - existing_set
    update_set = defined_set & existing_set
    delete_set = existing_set - defined_set

    create_list = [item for item in defined if item[compare_key] in create_set]
    for object_params in create_list:
        if parent_id:
            object_params.update({c_def['pid_ref_name']: parent_id})

        if 'pid_ref_name2' in c_def:
            object_params.update({c_def['pid_ref_name2']: template_id})

        _adjust_object_lists(object_params)

        if not dry_run:
            object_create = __salt__['zabbix.run_query'](c_def['qtype'] + '.create', object_params, **kwargs)
            if object_create:
                object_ids = object_create[c_def['res_id_name']]
                CHANGE_STACK.append({'component': component, 'action': 'create', 'params': object_params,
                                     c_def['filter']: object_params[c_def['filter']], 'object_id': object_ids})
        else:
            CHANGE_STACK.append({'component': component, 'action': 'create', 'params': object_params,
                                 'object_id': 'CREATED '+TEMPLATE_COMPONENT_DEF[component]['qtype']+' ID'})

    delete_list = [item for item in existing if item[compare_key] in delete_set]
    for object_del in delete_list:
        object_id_name = zabbix_id_mapper[c_def['qtype']]
        CHANGE_STACK.append({'component': component, 'action': 'delete', 'params': [object_del[object_id_name]]})
        if not dry_run:
            __salt__['zabbix.run_query'](c_def['qtype'] + '.delete', [object_del[object_id_name]], **kwargs)

    for object_name in update_set:
        ditem = next((item for item in defined if item[compare_key] == object_name), None)
        eitem = next((item for item in existing if item[compare_key] == object_name), None)
        diff_params = __salt__['zabbix.compare_params'](ditem, eitem, True)

        if diff_params['new']:
            diff_params['new'][zabbix_id_mapper[c_def['qtype']]] = eitem[zabbix_id_mapper[c_def['qtype']]]
            diff_params['old'][zabbix_id_mapper[c_def['qtype']]] = eitem[zabbix_id_mapper[c_def['qtype']]]
            _adjust_object_lists(diff_params['new'])
            _adjust_object_lists(diff_params['old'])
            CHANGE_STACK.append({'component': component, 'action': 'update', 'params': diff_params['new']})

            if not dry_run:
                __salt__['zabbix.run_query'](c_def['qtype'] + '.update', diff_params['new'], **kwargs)


def is_present(name, **kwargs):
    '''
    Check if Zabbix Template already exists.

    :param name: Zabbix Template name
    :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring)
    :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring)
    :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring)

    .. code-block:: yaml

        does_zabbix-template-exist:
            zabbix_template.is_present:
                - name: Template OS Linux
    '''
    ret = {'name': name, 'result': False, 'comment': '', 'changes': {}}

    try:
        object_id = __salt__['zabbix.get_object_id_by_params']('template', {'filter': {'name': name}}, **kwargs)
    except SaltException:
        object_id = False

    if not object_id:
        ret['result'] = False
        ret['comment'] = 'Zabbix Template "{0}" does not exist.'.format(name)
    else:
        ret['result'] = True
        ret['comment'] = 'Zabbix Template "{0}" exists.'.format(name)

    return ret


# pylint: disable=too-many-statements,too-many-locals
def present(name, params, static_host_list=True, **kwargs):
    '''
    Creates Zabbix Template object or if differs update it according defined parameters. See Zabbix API documentation.

    Zabbix API version: >3.0

    :param name: Zabbix Template name
    :param params: Additional parameters according to Zabbix API documentation
    :param static_host_list: If hosts assigned to the template are controlled
        only by this state or can be also assigned externally
    :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring)
    :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring)
    :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring)

    .. note::

        If there is a need to get a value from current zabbix online (e.g. ids of host groups you want the template
        to be associated with), put a dictionary with two keys "query_object" and "query_name" instead of the value.
        In this example we want to create template named "Testing Template", assign it to hostgroup Templates,
        link it to two ceph nodes and create a macro.

    .. note::

        IMPORTANT NOTE:
        Objects (except for template name) are identified by name (or by other key in some exceptional cases)
        so changing name of object means deleting old one and creating new one with new ID !!!

    .. note::

        NOT SUPPORTED FEATURES:
            - linked templates
            - trigger dependencies
            - groups and group prototypes for host prototypes

    SLS Example:

    .. code-block:: yaml

        zabbix-template-present:
            zabbix_template.present:
                - name: Testing Template
                # Do not touch existing assigned hosts
                # True will detach all other hosts than defined here
                - static_host_list: False
                - params:
                    description: Template for Ceph nodes
                    groups:
                        # groups must already exist
                        # template must be at least in one hostgroup
                        - groupid:
                            query_object: hostgroup
                            query_name: Templates
                    macros:
                        - macro: "{$CEPH_CLUSTER_NAME}"
                          value: ceph
                    hosts:
                        # hosts must already exist
                        - hostid:
                            query_object: host
                            query_name: ceph-osd-01
                        - hostid:
                            query_object: host
                            query_name: ceph-osd-02
                    # templates:
                    # Linked templates - not supported by state module but can be linked manually (will not be touched)

                    applications:
                        - name: Ceph OSD
                    items:
                        - name: Ceph OSD avg fill item
                          key_: ceph.osd_avg_fill
                          type: 2
                          value_type: 0
                          delay: 60
                          units: '%'
                          description: 'Average fill of OSD'
                          applications:
                              - applicationid:
                                  query_object: application
                                  query_name: Ceph OSD
                    triggers:
                        - description: "Ceph OSD filled more that 90%"
                          expression: "{{'{'}}Testing Template:ceph.osd_avg_fill.last(){{'}'}}>90"
                          priority: 4
                    discoveries:
                        - name: Mounted filesystem discovery
                          key_: vfs.fs.discovery
                          type: 0
                          delay: 60
                          itemprototypes:
                              - name: Free disk space on {{'{#'}}FSNAME}
                                key_: vfs.fs.size[{{'{#'}}FSNAME},free]
                                type: 0
                                value_type: 3
                                delay: 60
                                applications:
                                    - applicationid:
                                        query_object: application
                                        query_name: Ceph OSD
                          triggerprototypes:
                              - description: "Free disk space is less than 20% on volume {{'{#'}}FSNAME{{'}'}}"
                                expression: "{{'{'}}Testing Template:vfs.fs.size[{{'{#'}}FSNAME},free].last(){{'}'}}<20"
                    graphs:
                        - name: Ceph OSD avg fill graph
                          width: 900
                          height: 200
                          graphtype: 0
                          gitems:
                              - color: F63100
                                itemid:
                                  query_object: item
                                  query_name: Ceph OSD avg fill item
                    screens:
                        - name: Ceph
                          hsize: 1
                          vsize: 1
                          screenitems:
                              - x: 0
                                y: 0
                                resourcetype: 0
                                resourceid:
                                    query_object: graph
                                    query_name: Ceph OSD avg fill graph
    '''
    zabbix_id_mapper = __salt__['zabbix.get_zabbix_id_mapper']()

    dry_run = __opts__['test']
    ret = {'name': name, 'result': False, 'comment': '', 'changes': {}}
    params['host'] = name

    del CHANGE_STACK[:]

    # Divide template yaml definition into parts
    # - template definition itself
    # - simple template components
    # - components that have other sub-components
    #   (e.g. discoveries - where parent ID is needed in advance for sub-component manipulation)
    template_definition = {}
    template_components = {}
    discovery_components = []

    for attr in params:
        if attr in TEMPLATE_COMPONENT_ORDER and six.text_type(attr) != 'discoveries':
            template_components[attr] = params[attr]

        elif six.text_type(attr) == 'discoveries':
            d_rules = []
            for d_rule in params[attr]:
                d_rule_components = {'query_pid': {'component': attr,
                                                   'filter_val': d_rule[TEMPLATE_COMPONENT_DEF[attr]['filter']]}}
                for proto_name in DISCOVERYRULE_COMPONENT_ORDER:
                    if proto_name in d_rule:
                        d_rule_components[proto_name] = d_rule[proto_name]
                        del d_rule[proto_name]

                discovery_components.append(d_rule_components)
                d_rules.append(d_rule)

            template_components[attr] = d_rules

        else:
            template_definition[attr] = params[attr]

    # if a component is not defined, it means to remove existing items during update (empty list)
    for attr in TEMPLATE_COMPONENT_ORDER:
        if attr not in template_components:
            template_components[attr] = []

    # if a component is not defined, it means to remove existing items during update (empty list)
    for attr in TEMPLATE_RELATIONS:
        template_definition[attr] = params[attr] if attr in params and params[attr] else []

    defined_obj = __salt__['zabbix.substitute_params'](template_definition, **kwargs)
    log.info('SUBSTITUTED template_definition: %s', six.text_type(json.dumps(defined_obj, indent=4)))

    tmpl_get = __salt__['zabbix.run_query']('template.get',
                                            {'output': 'extend', 'selectGroups': 'groupid', 'selectHosts': 'hostid',
                                             'selectTemplates': 'templateid', 'selectMacros': 'extend',
                                             'filter': {'host': name}},
                                            **kwargs)
    log.info('TEMPLATE get result: %s', six.text_type(json.dumps(tmpl_get, indent=4)))

    existing_obj = __salt__['zabbix.substitute_params'](tmpl_get[0], **kwargs) \
        if tmpl_get and len(tmpl_get) == 1 else False

    if existing_obj:
        template_id = existing_obj[zabbix_id_mapper['template']]

        if not static_host_list:
            # Prepare objects for comparison
            defined_wo_hosts = defined_obj
            if 'hosts' in defined_obj:
                defined_hosts = defined_obj['hosts']
                del defined_wo_hosts['hosts']
            else:
                defined_hosts = []

            existing_wo_hosts = existing_obj
            if 'hosts' in existing_obj:
                existing_hosts = existing_obj['hosts']
                del existing_wo_hosts['hosts']
            else:
                existing_hosts = []

            # Compare host list separately from the rest of the object comparison since the merged list is needed for
            # update
            hosts_list = _diff_and_merge_host_list(defined_hosts, existing_hosts)

            # Compare objects without hosts
            diff_params = __salt__['zabbix.compare_params'](defined_wo_hosts, existing_wo_hosts, True)

            # Merge comparison results together
            if ('new' in diff_params and 'hosts' in diff_params['new']) or hosts_list:
                diff_params['new']['hosts'] = hosts_list

        else:
            diff_params = __salt__['zabbix.compare_params'](defined_obj, existing_obj, True)

        if diff_params['new']:
            diff_params['new'][zabbix_id_mapper['template']] = template_id
            diff_params['old'][zabbix_id_mapper['template']] = template_id
            log.info('TEMPLATE: update params: %s', six.text_type(json.dumps(diff_params, indent=4)))

            CHANGE_STACK.append({'component': 'template', 'action': 'update', 'params': diff_params['new']})
            if not dry_run:
                tmpl_update = __salt__['zabbix.run_query']('template.update', diff_params['new'], **kwargs)
                log.info('TEMPLATE update result: %s', six.text_type(tmpl_update))

    else:
        CHANGE_STACK.append({'component': 'template', 'action': 'create', 'params': defined_obj})
        if not dry_run:
            tmpl_create = __salt__['zabbix.run_query']('template.create', defined_obj, **kwargs)
            log.info('TEMPLATE create result: %s', six.text_type(tmpl_create))
            if tmpl_create:
                template_id = tmpl_create['templateids'][0]

    log.info('\n\ntemplate_components: %s', json.dumps(template_components, indent=4))
    log.info('\n\ndiscovery_components: %s', json.dumps(discovery_components, indent=4))
    log.info('\n\nCurrent CHANGE_STACK: %s', six.text_type(json.dumps(CHANGE_STACK, indent=4)))

    if existing_obj or not dry_run:
        for component in TEMPLATE_COMPONENT_ORDER:
            log.info('\n\n\n\n\nCOMPONENT: %s\n\n', six.text_type(json.dumps(component)))
            # 1) query for components which belongs to the template
            existing_c_list = _get_existing_template_c_list(component, template_id, **kwargs)
            existing_c_list_subs = __salt__['zabbix.substitute_params'](existing_c_list, **kwargs) \
                if existing_c_list else []

            if component in template_components:
                defined_c_list_subs = __salt__['zabbix.substitute_params'](
                    template_components[component],
                    extend_params={TEMPLATE_COMPONENT_DEF[component]['qselectpid']: template_id},
                    filter_key=TEMPLATE_COMPONENT_DEF[component]['filter'],
                    **kwargs)
            else:
                defined_c_list_subs = []
            # 2) take lists of particular component and compare -> do create, update and delete actions
            _manage_component(component, template_id, defined_c_list_subs, existing_c_list_subs, **kwargs)

        log.info('\n\nCurrent CHANGE_STACK: %s', six.text_type(json.dumps(CHANGE_STACK, indent=4)))

        for d_rule_component in discovery_components:
            # query for parent id -> "query_pid": {"filter_val": "vfs.fs.discovery", "component": "discoveries"}
            q_def = d_rule_component['query_pid']
            c_def = TEMPLATE_COMPONENT_DEF[q_def['component']]
            q_object = c_def['qtype']
            q_params = dict(c_def['output'])
            q_params.update({c_def['qselectpid']: template_id})
            q_params.update({'filter': {c_def['filter']: q_def['filter_val']}})

            parent_id = __salt__['zabbix.get_object_id_by_params'](q_object, q_params, **kwargs)

            for proto_name in DISCOVERYRULE_COMPONENT_ORDER:
                log.info('\n\n\n\n\nPROTOTYPE_NAME: %s\n\n', six.text_type(json.dumps(proto_name)))
                existing_p_list = _get_existing_template_c_list(proto_name, parent_id, **kwargs)
                existing_p_list_subs = __salt__['zabbix.substitute_params'](existing_p_list, **kwargs)\
                    if existing_p_list else []

                if proto_name in d_rule_component:
                    defined_p_list_subs = __salt__['zabbix.substitute_params'](
                        d_rule_component[proto_name],
                        extend_params={c_def['qselectpid']: template_id},
                        **kwargs)
                else:
                    defined_p_list_subs = []

                _manage_component(proto_name,
                                  parent_id,
                                  defined_p_list_subs,
                                  existing_p_list_subs,
                                  template_id=template_id,
                                  **kwargs)

    log.info('\n\nCurrent CHANGE_STACK: %s', six.text_type(json.dumps(CHANGE_STACK, indent=4)))

    if not CHANGE_STACK:
        ret['result'] = True
        ret['comment'] = 'Zabbix Template "{0}" already exists and corresponds to a definition.'.format(name)
    else:
        tmpl_action = next((item for item in CHANGE_STACK
                            if item['component'] == 'template' and item['action'] == 'create'), None)
        if tmpl_action:
            ret['result'] = True
            if dry_run:
                ret['comment'] = 'Zabbix Template "{0}" would be created.'.format(name)
                ret['changes'] = {name: {'old': 'Zabbix Template "{0}" does not exist.'.format(name),
                                         'new': 'Zabbix Template "{0}" would be created '
                                                'according definition.'.format(name)}}
            else:
                ret['comment'] = 'Zabbix Template "{0}" created.'.format(name)
                ret['changes'] = {name: {'old': 'Zabbix Template "{0}" did not exist.'.format(name),
                                         'new': 'Zabbix Template "{0}" created according definition.'.format(name)}}
        else:
            ret['result'] = True
            if dry_run:
                ret['comment'] = 'Zabbix Template "{0}" would be updated.'.format(name)
                ret['changes'] = {name: {'old': 'Zabbix Template "{0}" differs.'.format(name),
                                         'new': 'Zabbix Template "{0}" would be updated '
                                                'according definition.'.format(name)}}
            else:
                ret['comment'] = 'Zabbix Template "{0}" updated.'.format(name)
                ret['changes'] = {name: {'old': 'Zabbix Template "{0}" differed.'.format(name),
                                         'new': 'Zabbix Template "{0}" updated according definition.'.format(name)}}

    return ret


def absent(name, **kwargs):
    '''
    Makes the Zabbix Template to be absent (either does not exist or delete it).

    :param name: Zabbix Template name
    :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring)
    :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring)
    :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring)

    .. code-block:: yaml

        zabbix-template-absent:
            zabbix_template.absent:
                - name: Ceph OSD
    '''
    dry_run = __opts__['test']
    ret = {'name': name, 'result': False, 'comment': '', 'changes': {}}

    try:
        object_id = __salt__['zabbix.get_object_id_by_params']('template', {'filter': {'name': name}}, **kwargs)
    except SaltException:
        object_id = False

    if not object_id:
        ret['result'] = True
        ret['comment'] = 'Zabbix Template "{0}" does not exist.'.format(name)
    else:
        if dry_run:
            ret['result'] = True
            ret['comment'] = 'Zabbix Template "{0}" would be deleted.'.format(name)
            ret['changes'] = {name: {'old': 'Zabbix Template "{0}" exists.'.format(name),
                                     'new': 'Zabbix Template "{0}" would be deleted.'.format(name)}}
        else:
            tmpl_delete = __salt__['zabbix.run_query']('template.delete', [object_id], **kwargs)
            if tmpl_delete:
                ret['result'] = True
                ret['comment'] = 'Zabbix Template "{0}" deleted.'.format(name)
                ret['changes'] = {name: {'old': 'Zabbix Template "{0}" existed.'.format(name),
                                         'new': 'Zabbix Template "{0}" deleted.'.format(name)}}

    return ret
