#
# -*- coding: utf-8 -*-
# Copyright 2019 Red Hat
# GNU General Public License v3.0+
# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)#!/usr/bin/python

"""
The nxos vlans fact class
It is in this file the configuration is collected from the device
for a given resource, parsed, and the facts tree is populated
based on the configuration.
"""
from __future__ import absolute_import, division, print_function


__metaclass__ = type

import ast
import re

from copy import deepcopy

from ansible_collections.ansible.netcommon.plugins.module_utils.network.common import utils
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
    parse_conf_arg,
    parse_conf_cmd_arg,
)

from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.argspec.vlans.vlans import (
    VlansArgs,
)


class VlansFacts(object):
    """The nxos vlans fact class"""

    def __init__(self, module, subspec="config", options="options"):
        self._module = module
        self.argument_spec = VlansArgs.argument_spec
        spec = deepcopy(self.argument_spec)
        if subspec:
            if options:
                facts_argument_spec = spec[subspec][options]
            else:
                facts_argument_spec = spec[subspec]
        else:
            facts_argument_spec = spec

        self.generated_spec = utils.generate_dict(facts_argument_spec)

    def get_device_data(self, connection, show_cmd):
        """Wrapper method for `connection.get()`
        This exists solely to allow the unit test framework to mock device connection calls.
        """
        return connection.get(show_cmd)

    def populate_facts(self, connection, ansible_facts, data=None):
        """Populate the facts for vlans
        :param connection: the device connection
        :param data: previously collected conf
        :rtype: dictionary
        :returns: facts
        """
        objs = []
        # **TBD**
        # N7K EOL/legacy image 6.2 does not support show vlan | json output.
        # If support is still required for this image then:
        # - Wrapp the json calls below in a try/except
        # - When excepted, use a helper method to parse the run_cfg_output,
        #   using the run_cfg_output data to generate compatible json data that
        #   can be read by normalize_table_data.
        if not data:
            # Use structured for most of the vlan parameter states.
            # This data is consistent across the supported nxos platforms.
            try:
                # Not all devices support | json-pretty but is a workaround for
                # libssh issue https://github.com/ansible/pylibssh/issues/208
                structured = self.get_device_data(connection, "show vlan | json-pretty")
            except Exception:
                # When json-pretty is not supported, we fall back to | json
                structured = self.get_device_data(connection, "show vlan | json")

            # Raw cli config is needed for mapped_vni, which is not included in structured.
            run_cfg_output = self.get_device_data(connection, "show running-config | section ^vlan")
        else:
            running_config = data.split("\n\n")
            structured, run_cfg_output = running_config[0], running_config[1]

        # Create a single dictionary from all data sources
        data = self.normalize_table_data(structured, run_cfg_output)

        for vlan in data:
            obj = self.render_config(self.generated_spec, vlan)
            if obj:
                objs.append(obj)

        ansible_facts["ansible_network_resources"].pop("vlans", None)
        facts = {}
        if objs:
            facts["vlans"] = []
            params = utils.validate_config(self.argument_spec, {"config": objs})
            for cfg in params["config"]:
                facts["vlans"].append(utils.remove_empties(cfg))
        ansible_facts["ansible_network_resources"].update(facts)
        return ansible_facts

    def render_config(self, spec, vlan):
        """
        Render config as dictionary structure and delete keys
          from spec for null values
        :param spec: The facts tree, generated from the argspec
        :param vlan: structured data vlan settings (dict) and raw cfg from device
        :rtype: dictionary
        :returns: The generated config
        Sample inputs: test/units/modules/network/nxos/fixtures/nxos_vlans/show_vlan
        """
        obj = deepcopy(spec)

        obj["vlan_id"] = vlan["vlan_id"]

        # name: 'VLAN000x' (default name) or custom name
        name = vlan["vlanshowbr-vlanname"]
        if name and re.match("VLAN%04d" % int(vlan["vlan_id"]), name):
            name = None
        obj["name"] = name

        # mode: 'ce-vlan' or 'fabricpath-vlan'
        obj["mode"] = vlan["vlanshowinfo-vlanmode"].replace("-vlan", "")

        # enabled: shutdown, noshutdown
        obj["enabled"] = True if "noshutdown" in vlan["vlanshowbr-shutstate"] else False

        # state: active, suspend
        obj["state"] = vlan["vlanshowbr-vlanstate"]

        # non-structured data
        obj["mapped_vni"] = parse_conf_arg(vlan["run_cfg"], "vn-segment")

        return utils.remove_empties(obj)

    def normalize_table_data(self, structured, run_cfg_output):
        """Normalize structured output and raw running-config output into
        a single dict to simplify render_config usage.
        This is needed because:
        - The NXOS devices report most of the vlan settings within two
          structured data keys: 'vlanbrief' and 'mtuinfo', but the output is
          incomplete and therefore raw running-config data is also needed.
        - running-config by itself is insufficient because of major differences
          in the cli config syntax across platforms.
        - Thus a helper method combines settings from the separate top-level keys,
          and adds a 'run_cfg' key containing raw cli from the device.
        """
        # device output may be string, convert to list
        structured = ast.literal_eval(str(structured))

        vlanbrief = []
        mtuinfo = []
        if "TABLE_vlanbrief" in structured:
            # SAMPLE: {"TABLE_vlanbriefid": {"ROW_vlanbriefid": {
            #   "vlanshowbr-vlanid": "4", "vlanshowbr-vlanid-utf": "4",
            #   "vlanshowbr-vlanname": "VLAN0004", "vlanshowbr-vlanstate": "active",
            #   "vlanshowbr-shutstate": "noshutdown"}},
            vlanbrief = structured["TABLE_vlanbrief"]["ROW_vlanbrief"]

            # SAMPLE: "TABLE_mtuinfoid": {"ROW_mtuinfoid": {
            #   "vlanshowinfo-vlanid": "4", "vlanshowinfo-media-type": "enet",
            #   "vlanshowinfo-vlanmode": "ce-vlan"}}
            mtuinfo = structured["TABLE_mtuinfo"]["ROW_mtuinfo"]

        if type(vlanbrief) is not list:
            # vlanbrief is not a list when only one vlan is found.
            vlanbrief = [vlanbrief]
            mtuinfo = [mtuinfo]

        # split out any per-vlan cli config
        run_cfg_list = re.split(r"[\n^]vlan ", run_cfg_output)

        # Create a list of vlan dicts where each dict contains vlanbrief,
        # mtuinfo, and non-structured running-config data for one vlan.
        vlans = []
        for index, v in enumerate(vlanbrief):
            v["vlan_id"] = v.get("vlanshowbr-vlanid-utf")
            vlan = {}
            vlan.update(v)
            vlan.update(mtuinfo[index])

            vlan["run_cfg"] = ""
            for item in run_cfg_list:
                # Sample match lines
                # 202\n  name Production-Segment-100101\n  vn-segment 100101
                # 5\n  state suspend\n  shutdown\n  name test-changeme\n  vn-segment 942
                pattern = r"^{0}\s+\S.*vn-segment".format(v["vlan_id"])
                if re.search(pattern, item, flags=re.DOTALL):
                    vlan["run_cfg"] = item
                    break

            vlans.append(vlan)
        return vlans
