# (c) 2018, Ansible by Red Hat, inc
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import os
import sys
import collections

from ansible import constants as C
from ansible.plugins.action import ActionBase
from ansible.module_utils.common._collections_compat import Mapping
from ansible.module_utils.six import iteritems, string_types
from ansible.module_utils._text import to_text
from ansible.errors import AnsibleError
from ansible.utils.display import Display

try:
    from ansible.module_utils.network.common.utils import to_list
except ImportError:
    # keep role compatible with Ansible 2.4
    from ansible.module_utils.network_common import to_list

sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.path.pardir, 'lib'))
from network_engine.plugins import template_loader, parser_loader
from network_engine.utils import dict_merge, generate_source_path

display = Display()


def warning(msg):
    if C.ACTION_WARNINGS:
        display.warning(msg)


class ActionModule(ActionBase):

    VALID_FILE_EXTENSIONS = ('.yaml', '.yml', '.json')
    VALID_GROUP_DIRECTIVES = ('pattern_group', 'block')
    VALID_ACTION_DIRECTIVES = ('parser_metadata', 'pattern_match', 'set_vars', 'json_template')
    VALID_DIRECTIVES = VALID_GROUP_DIRECTIVES + VALID_ACTION_DIRECTIVES
    VALID_EXPORT_AS = ('list', 'elements', 'dict', 'object', 'hash')

    def run(self, tmp=None, task_vars=None):
        if task_vars is None:
            task_vars = dict()

        result = super(ActionModule, self).run(tmp, task_vars)

        try:
            source_dir = self._task.args.get('dir')
            source_file = self._task.args.get('file')
            content = self._task.args['content']
        except KeyError as exc:
            return {'failed': True, 'msg': 'missing required argument: %s' % exc}

        if source_dir and source_file:
            return {'failed': True, 'msg': '`dir` and `file` are mutually exclusive arguments'}

        if source_dir:
            sources = self.get_files(to_list(source_dir))
        else:
            if source_file:
                sources = to_list(source_file)
            else:
                searchpath = []
                searchpath = task_vars.get('ansible_search_path', [])
                if not searchpath:
                    searchpath.append(self._loader._basedir)

                if 'parser_templates' in os.listdir(searchpath[0]):
                    subdir_searchpath = os.path.join(searchpath[0], 'parser_templates')

                    # parser in {{ playbook_dir }}/parser_templates/{{ ansible_network_os }}
                    if task_vars['ansible_network_os'] in os.listdir(subdir_searchpath):
                        newsearchpath = os.path.join(subdir_searchpath, task_vars['ansible_network_os'])
                        sources = self.get_parser(path=newsearchpath)

                    # parser in {{ playbook_dir }}/parser_templates
                    else:
                        sources = self.get_parser(path=subdir_searchpath)

                # parser in {{ playbook_dir }}
                else:
                    sources = self.get_parser(path=searchpath[0])

        facts = {}

        self.template = template_loader.get('json_template', self._templar)

        paths = self._task.get_search_path()
        for src in sources:
            src = generate_source_path(paths, src)
            if src is None:
                raise AnsibleError("src [%s] is either missing or invalid" % src)

            tasks = self._loader.load_from_file(src)

            self.ds = {'content': content}
            self.ds.update(task_vars)

            for task in tasks:
                name = task.pop('name', None)
                display.vvvv('processing directive: %s' % name)

                register = task.pop('register', None)
                extend = task.pop('extend', None)
                if extend:
                    extend = self.template(extend, self.ds)

                export = task.pop('export', False)
                export_as = task.pop('export_as', 'list')
                export_as = self.template(export_as, self.ds)
                if export_as not in self.VALID_EXPORT_AS:
                    raise AnsibleError('invalid value for export_as, got %s' % export_as)

                if 'export_facts' in task:
                    task['set_vars'] = task.pop('export_facts')
                    export = True
                elif 'set_vars' not in task:
                    if export and not register:
                        warning('entry will not be exported due to missing register option')

                when = task.pop('when', None)
                if when is not None:
                    if not self._check_conditional(when, self.ds):
                        display.vvv('command_parser: skipping task [%s] due to conditional check' % name)
                        continue

                loop = task.pop('loop', None)
                loop_var = task.pop('loop_control', {}).get('loop_var') or 'item'

                if loop is not None:
                    loop = self.template(loop, self.ds)
                    if not loop:
                        display.vvv('command_parser: loop option was defined but no loop data found')
                    res = list()

                    if loop:
                        # loop is a hash so break out key and value
                        if isinstance(loop, Mapping):
                            for loop_key, loop_value in iteritems(loop):
                                self.ds[loop_var] = {'key': loop_key, 'value': loop_value}
                                resp = self._process_directive(task)
                                res.append(resp)

                        # loop is either a list or a string
                        else:
                            for loop_item in loop:
                                self.ds[loop_var] = loop_item
                                resp = self._process_directive(task)
                                res.append(resp)

                        if 'set_vars' in task:
                            if register:
                                self.ds[register] = res
                                if export:
                                    if extend:
                                        facts.update(self.merge_facts(task_vars, extend, register, res))
                                    else:
                                        facts[register] = res
                            else:
                                self.ds.update(res)
                                if export:
                                    facts.update(res)
                        elif register:
                            self.ds[register] = res
                            if export:
                                if export_as in ('dict', 'hash', 'object'):
                                    if extend:
                                        facts.update(self.merge_facts(task_vars, extend, register, res, expand=True))
                                    else:
                                        if register not in facts:
                                            facts[register] = {}
                                        for item in res:
                                            facts[register] = self.rec_update(facts[register], item)
                                else:
                                    if extend:
                                        facts.update(self.merge_facts(task_vars, extend, register, res))
                                    else:
                                        facts[register] = res
                else:
                    res = self._process_directive(task)
                    if 'set_vars' in task:
                        if register:
                            self.ds[register] = res
                            if export:
                                if extend:
                                    facts.update(self.merge_facts(task_vars, extend, register, res))
                                else:
                                    facts[register] = res
                        else:
                            self.ds.update(res)
                            if export:
                                facts.update(res)
                    elif res and register:
                        self.ds[register] = res
                        if export:
                            if register:
                                if extend:
                                    facts.update(self.merge_facts(task_vars, extend, register, res))
                                else:
                                    facts[register] = res
                            else:
                                for r in to_list(res):
                                    for k, v in iteritems(r):
                                        facts.update({to_text(k): v})

                task_vars.update(facts)

        result.update({
            'ansible_facts': facts,
            'included': sources
        })

        return result

    def merge_facts(self, task_vars, extend, register, res, expand=False):
        update = self.build_update(extend, register, res, expand)
        root = extend.split('.')[0]
        current = {root: task_vars.get(root, {})}
        return dict_merge(current, update)

    def build_update(self, path, child, value, expand=False):
        """Build an update based on the current results

        This method will take the current results and build a nested dict
        object.  The keys for the nested dict object are identified by
        path.

        :param path: The path of the nest keys

        :param child: The child key name to assign the value to

        :param value: The value to assign to the child key

        :param expand: When set to True, this will iterate over the value

        :returns: A nest dict object
        """
        update_set = dict()
        working_set = update_set

        if expand is True:
            for key in path.split('.'):
                working_set[key] = dict()
                working_set = working_set[key]
            working_set[child] = {}
            for item in value:
                working_set[child] = self.rec_update(working_set[child], item)
        else:
            for key in path.split('.'):
                working_set[key] = dict()
                working_set = working_set[key]
            working_set[child] = value

        return update_set

    def get_parser(self, path):
        sources = list()
        src_file = list()

        for i in os.listdir(path):
            if i.startswith('show_'):
                f, ext = os.path.splitext(i)
                if ext in self.VALID_FILE_EXTENSIONS:
                    src_file.append(i)

        if len(src_file) == 1:
            sources.append(os.path.join(path, src_file[0]))
        elif len(src_file) == 0:
            raise AnsibleError("no parser file found in {0}, please create a parser".format(path))
        else:
            raise AnsibleError("too many files in {0}, please use `file` or `dir` parameter".format(path))

        return sources

    def get_files(self, source_dirs):
        include_files = list()
        _processed = set()

        for source_dir in source_dirs:
            if not os.path.isdir(source_dir):
                raise AnsibleError('%s does not appear to be a valid directory' % source_dir)

            for filename in os.listdir(source_dir):
                fn, fext = os.path.splitext(filename)
                if fn not in _processed:
                    _processed.add(fn)

                    filename = os.path.join(source_dir, filename)

                    if not os.path.isfile(filename) or fext not in self.VALID_FILE_EXTENSIONS:
                        continue
                    else:
                        include_files.append(filename)

        return include_files

    def rec_update(self, d, u):
        for k, v in iteritems(u):
            if isinstance(v, Mapping):
                d[k] = self.rec_update(d.get(k, {}), v)
            else:
                d[k] = v
        return d

    def do_pattern_group(self, block):

        results = list()
        registers = {}

        for entry in block:
            task = entry.copy()

            name = task.pop('name', None)
            display.vvv("command_parser: starting pattern_match [%s] in pattern_group" % name)

            register = task.pop('register', None)

            when = task.pop('when', None)
            if when is not None:
                if not self._check_conditional(when, self.ds):
                    warning('skipping task due to conditional check failure')
                    continue

            loop = task.pop('loop', None)
            if loop:
                loop = self.template(loop, self.ds)

            loop_var = task.pop('loop_control', {}).get('loop_var') or 'item'
            display.vvvv('command_parser: loop_var is %s' % loop_var)

            if not set(task).issubset(('pattern_group', 'pattern_match')):
                raise AnsibleError('invalid directive specified')

            if 'pattern_group' in task:
                if loop and isinstance(loop, collections.Iterable) and not isinstance(loop, string_types):
                    res = list()
                    for loop_item in loop:
                        self.ds[loop_var] = loop_item
                        res.append(self.do_pattern_group(task['pattern_group']))
                else:
                    res = self.do_pattern_group(task['pattern_group'])

                if res:
                    results.append(res)
                if register:
                    registers[register] = res

            elif isinstance(loop, collections.Iterable) and not isinstance(loop, string_types):
                loop_result = list()

                for loop_item in loop:
                    self.ds[loop_var] = loop_item
                    loop_result.append(self._process_directive(task))

                results.append(loop_result)

                if register:
                    registers[register] = loop_result

            else:
                res = self._process_directive(task)
                if res:
                    results.append(res)
                if register:
                    registers[register] = res

        return registers

    def _process_directive(self, task):
        for directive, args in iteritems(task):
            if directive == 'block':
                display.deprecated('`block` is not longer supported, use `pattern_group` instead', version=2.6)
                directive = 'pattern_group'

            if directive not in self.VALID_DIRECTIVES:
                raise AnsibleError('invalid directive in parser: %s' % directive)

            meth = getattr(self, 'do_%s' % directive)

            if meth:
                if directive in self.VALID_GROUP_DIRECTIVES:
                    return meth(args)
                elif directive in self.VALID_ACTION_DIRECTIVES:
                    return meth(**args)
            else:
                raise AnsibleError('invalid directive: %s' % directive)

    def do_parser_metadata(self, version=None, command=None, network_os=None):
        if version:
            display.vvv('command_parser: using parser version %s' % version)

        if network_os not in (None, self.ds['ansible_network_os']):
            raise AnsibleError('parser expected %s, got %s' % (network_os, self.ds['ansible_network_os']))

    def do_pattern_match(self, regex, content=None, match_all=None, match_until=None, match_greedy=None):
        content = self.template(content, self.ds) or self.template("{{ content }}", self.ds)
        regex = self.template(regex, self.ds)
        parser = parser_loader.get('pattern_match', content)
        return parser.match(regex, match_all, match_until, match_greedy)

    def do_json_template(self, template):
        return self.template.run(template, self.ds)

    def do_set_vars(self, **kwargs):
        return self.template(kwargs, self.ds)

    def _check_conditional(self, when, variables):
        conditional = "{%% if %s %%}True{%% else %%}False{%% endif %%}"
        return self.template(conditional % when, variables)
