# -*- 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)
from __future__ import absolute_import, division, print_function


__metaclass__ = type

import platform
import re

from ansible.module_utils.six import iteritems

from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import (
    get_capabilities,
    get_config,
    run_commands,
)
from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.utils.utils import (
    get_interface_type,
    normalize_interface,
)


g_config = None


class FactsBase(object):
    def __init__(self, module):
        self.module = module
        self.warnings = list()
        self.facts = dict()
        self.capabilities = get_capabilities(self.module)

    def populate(self):
        pass

    def run(self, command, output="text"):
        command_string = command
        if output == "json":
            # Not all devices support | json-pretty but is a workaround for
            # libssh issue https://github.com/ansible/pylibssh/issues/208
            output = "json-pretty"

        resp = []
        command = {"command": command, "output": output}
        try:
            resp = run_commands(self.module, [command], check_rc="retry_json")
        except Exception:
            if output == "json-pretty":
                # reattempt with | json
                resp = run_commands(
                    self.module,
                    [{"command": command, "output": "json"}],
                    check_rc="retry_json",
                )

        try:
            return resp[0]
        except IndexError:
            self.warnings.append(
                "command %s failed, facts for this command will not be populated" % command_string,
            )
            return None

    def get_config(self):
        global g_config
        if not g_config:
            g_config = get_config(self.module)
        return g_config

    def transform_dict(self, data, keymap):
        transform = dict()
        for key, fact in keymap:
            if key in data:
                transform[fact] = data[key]
        return transform

    def transform_iterable(self, iterable, keymap):
        for item in iterable:
            yield self.transform_dict(item, keymap)


class Default(FactsBase):
    def populate(self):
        data = None
        data = self.run("show version")

        if data:
            self.facts["serialnum"] = self.parse_serialnum(data)

        data = self.run("show license host-id")
        if data:
            self.facts["license_hostid"] = self.parse_license_hostid(data)

        self.facts.update(self.platform_facts())

    def parse_serialnum(self, data):
        match = re.search(r"Processor Board ID\s*(\S+)", data, re.M)
        if match:
            return match.group(1)

    def platform_facts(self):
        platform_facts = {}

        resp = self.capabilities
        device_info = resp["device_info"]

        platform_facts["system"] = device_info["network_os"]

        for item in ("model", "image", "version", "platform", "hostname"):
            val = device_info.get("network_os_%s" % item)
            if val:
                platform_facts[item] = val

        platform_facts["api"] = resp["network_api"]
        platform_facts["python_version"] = platform.python_version()

        return platform_facts

    def parse_license_hostid(self, data):
        match = re.search(r"License hostid: VDH=(.+)$", data, re.M)
        if match:
            return match.group(1)


class Config(FactsBase):
    def populate(self):
        super(Config, self).populate()
        self.facts["config"] = self.get_config()


class Features(FactsBase):
    def populate(self):
        super(Features, self).populate()
        data = self.get_config()

        if data:
            features = []
            for line in data.splitlines():
                if line.startswith("feature"):
                    features.append(line.replace("feature", "").strip())

            self.facts["features_enabled"] = features


class Hardware(FactsBase):
    def populate(self):
        data = self.run("dir")
        if data:
            self.facts["filesystems"] = self.parse_filesystems(data)

        data = None
        data = self.run("show system resources", output="json")

        if data:
            if isinstance(data, dict):
                self.facts["memtotal_mb"] = int(data["memory_usage_total"]) / 1024
                self.facts["memfree_mb"] = int(data["memory_usage_free"]) / 1024
            else:
                self.facts["memtotal_mb"] = self.parse_memtotal_mb(data)
                self.facts["memfree_mb"] = self.parse_memfree_mb(data)

        data = None
        data = self.run("show processes cpu | json")

        if data:
            self.facts["cpu_utilization"] = self.parse_cpu_utilization(data)

    def parse_cpu_utilization(self, data):
        return {
            "core": {
                "five_minutes": int(data.get("fivemin_percent", 0)),
                "five_seconds": int(
                    data.get("fivesec_percent", 0),
                ),
                "five_seconds_interrupt": int(
                    data.get("fivesec_intr_percent", 0),
                ),
                "one_minute": int(data.get("onemin_percent", 0)),
            },
        }

    def parse_filesystems(self, data):
        return re.findall(r"^Usage for (\S+)//", data, re.M)

    def parse_memtotal_mb(self, data):
        match = re.search(r"(\S+)K(\s+|)total", data, re.M)
        if match:
            memtotal = match.group(1)
            return int(memtotal) / 1024

    def parse_memfree_mb(self, data):
        match = re.search(r"(\S+)K(\s+|)free", data, re.M)
        if match:
            memfree = match.group(1)
            return int(memfree) / 1024


class Interfaces(FactsBase):
    INTERFACE_MAP = frozenset(
        [
            ("state", "state"),
            ("desc", "description"),
            ("eth_bw", "bandwidth"),
            ("eth_duplex", "duplex"),
            ("eth_speed", "speed"),
            ("eth_mode", "mode"),
            ("eth_hw_addr", "macaddress"),
            ("eth_mtu", "mtu"),
            ("eth_hw_desc", "type"),
        ],
    )

    INTERFACE_SVI_MAP = frozenset(
        [
            ("svi_line_proto", "state"),
            ("svi_bw", "bandwidth"),
            ("svi_mac", "macaddress"),
            ("svi_mtu", "mtu"),
            ("type", "type"),
        ],
    )

    INTERFACE_IPV4_MAP = frozenset(
        [("eth_ip_addr", "address"), ("eth_ip_mask", "masklen")],
    )

    INTERFACE_SVI_IPV4_MAP = frozenset(
        [("svi_ip_addr", "address"), ("svi_ip_mask", "masklen")],
    )

    INTERFACE_IPV6_MAP = frozenset([("addr", "address"), ("prefix", "subnet")])

    def ipv6_structure_op_supported(self):
        data = self.capabilities
        if data:
            nxos_os_version = data["device_info"]["network_os_version"]
            unsupported_versions = ["I2", "F1", "A8"]
            for ver in unsupported_versions:
                if ver in nxos_os_version:
                    return False
            return True

    def populate(self):
        self.facts["all_ipv4_addresses"] = list()
        self.facts["all_ipv6_addresses"] = list()
        self.facts["neighbors"] = {}
        data = None

        data = self.run("show interface", output="json")

        if data:
            if isinstance(data, dict):
                self.facts["interfaces"] = self.populate_structured_interfaces(data)
            else:
                interfaces = self.parse_interfaces(data)
                self.facts["interfaces"] = self.populate_interfaces(interfaces)

        if self.ipv6_structure_op_supported():
            data = self.run("show ipv6 interface", output="json")
        else:
            data = None
        if data:
            if isinstance(data, dict):
                self.populate_structured_ipv6_interfaces(data)
            else:
                interfaces = self.parse_interfaces(data)
                self.populate_ipv6_interfaces(interfaces)

        data = self.run("show lldp neighbors", output="json")
        if data:
            if isinstance(data, dict):
                self.facts["neighbors"].update(
                    self.populate_structured_neighbors_lldp(data),
                )
            else:
                self.facts["neighbors"].update(self.populate_neighbors(data))

        data = self.run("show cdp neighbors detail", output="json")
        if data:
            if isinstance(data, dict):
                self.facts["neighbors"].update(
                    self.populate_structured_neighbors_cdp(data),
                )
            else:
                self.facts["neighbors"].update(self.populate_neighbors_cdp(data))

        self.facts["neighbors"].pop(None, None)  # Remove null key

    def populate_structured_interfaces(self, data):
        interfaces = dict()
        data = data["TABLE_interface"]["ROW_interface"]

        if isinstance(data, dict):
            data = [data]

        for item in data:
            name = item["interface"]

            intf = dict()
            if any(key.startswith("svi_") for key in item):
                intf.update(self.transform_dict(item, self.INTERFACE_SVI_MAP))
            else:
                intf.update(self.transform_dict(item, self.INTERFACE_MAP))

            if "eth_ip_addr" in item:
                intf["ipv4"] = self.transform_dict(item, self.INTERFACE_IPV4_MAP)
                self.facts["all_ipv4_addresses"].append(item["eth_ip_addr"])

            if "svi_ip_addr" in item:
                intf["ipv4"] = self.transform_dict(item, self.INTERFACE_SVI_IPV4_MAP)
                self.facts["all_ipv4_addresses"].append(item["svi_ip_addr"])

            interfaces[name] = intf

        return interfaces

    def populate_structured_ipv6_interfaces(self, data):
        try:
            data = data["TABLE_intf"]
            if data:
                if isinstance(data, dict):
                    data = [data]
                for item in data:
                    row_intf = item["ROW_intf"]
                    if isinstance(row_intf, dict):
                        row_intf = [row_intf]
                    for item in row_intf:
                        intf = self.facts["interfaces"][item["intf-name"]]
                        intf["ipv6"] = self.transform_dict(item, self.INTERFACE_IPV6_MAP)
                        try:
                            addr = item["addr"]
                        except KeyError:
                            addr = item["TABLE_addr"]["ROW_addr"]["addr"]
                        self.facts["all_ipv6_addresses"].append(addr)
            else:
                return ""
        except TypeError:
            return ""

    def populate_structured_neighbors_lldp(self, data):
        objects = dict()
        data = data["TABLE_nbor"]["ROW_nbor"]

        if isinstance(data, dict):
            data = [data]

        for item in data:
            local_intf = normalize_interface(item["l_port_id"])
            objects[local_intf] = list()
            nbor = dict()
            nbor["port"] = item["port_id"]
            nbor["host"] = nbor["sysname"] = item["chassis_id"]
            objects[local_intf].append(nbor)

        return objects

    def populate_structured_neighbors_cdp(self, data):
        objects = dict()
        data = data["TABLE_cdp_neighbor_detail_info"]["ROW_cdp_neighbor_detail_info"]

        if isinstance(data, dict):
            data = [data]

        for item in data:
            if "intf_id" in item:
                local_intf = item["intf_id"]
            else:
                # in some N7Ks the key has been renamed
                local_intf = item["interface"]
            objects[local_intf] = list()
            nbor = dict()
            nbor["port"] = item["port_id"]
            nbor["host"] = nbor["sysname"] = item["device_id"]
            objects[local_intf].append(nbor)

        return objects

    def parse_interfaces(self, data):
        parsed = dict()
        key = ""
        for line in data.split("\n"):
            if len(line) == 0:
                continue
            elif line.startswith("admin") or line[0] == " ":
                parsed[key] += "\n%s" % line
            else:
                match = re.match(r"^(\S+)", line)
                if match:
                    key = match.group(1)
                    if not key.startswith("admin") or not key.startswith(
                        "IPv6 Interface",
                    ):
                        parsed[key] = line
        return parsed

    def populate_interfaces(self, interfaces):
        facts = dict()
        for key, value in iteritems(interfaces):
            intf = dict()
            if get_interface_type(key) == "svi":
                intf["state"] = self.parse_state(key, value, intf_type="svi")
                intf["macaddress"] = self.parse_macaddress(value, intf_type="svi")
                intf["mtu"] = self.parse_mtu(value, intf_type="svi")
                intf["bandwidth"] = self.parse_bandwidth(value, intf_type="svi")
                intf["type"] = self.parse_type(value, intf_type="svi")
                if "Internet Address" in value:
                    intf["ipv4"] = self.parse_ipv4_address(value, intf_type="svi")
                facts[key] = intf
            else:
                intf["state"] = self.parse_state(key, value)
                intf["description"] = self.parse_description(value)
                intf["macaddress"] = self.parse_macaddress(value)
                intf["mode"] = self.parse_mode(value)
                intf["mtu"] = self.parse_mtu(value)
                intf["bandwidth"] = self.parse_bandwidth(value)
                intf["duplex"] = self.parse_duplex(value)
                intf["speed"] = self.parse_speed(value)
                intf["type"] = self.parse_type(value)
                if "Internet Address" in value:
                    intf["ipv4"] = self.parse_ipv4_address(value)
                facts[key] = intf

        return facts

    def parse_state(self, key, value, intf_type="ethernet"):
        match = None
        if intf_type == "svi":
            match = re.search(r"line protocol is\s*(\S+)", value, re.M)
        else:
            match = re.search(r"%s is\s*(\S+)" % key, value, re.M)

        if match:
            return match.group(1)

    def parse_macaddress(self, value, intf_type="ethernet"):
        match = None
        if intf_type == "svi":
            match = re.search(r"address is\s*(\S+)", value, re.M)
        else:
            match = re.search(r"address:\s*(\S+)", value, re.M)

        if match:
            return match.group(1)

    def parse_mtu(self, value, intf_type="ethernet"):
        match = re.search(r"MTU\s*(\S+)", value, re.M)
        if match:
            return match.group(1)

    def parse_bandwidth(self, value, intf_type="ethernet"):
        match = re.search(r"BW\s*(\S+)", value, re.M)
        if match:
            return match.group(1)

    def parse_type(self, value, intf_type="ethernet"):
        match = None
        if intf_type == "svi":
            match = re.search(r"Hardware is\s*(\S+)", value, re.M)
        else:
            match = re.search(r"Hardware:\s*(.+),", value, re.M)

        if match:
            return match.group(1)

    def parse_description(self, value, intf_type="ethernet"):
        match = re.search(r"Description: (.+)$", value, re.M)
        if match:
            return match.group(1)

    def parse_mode(self, value, intf_type="ethernet"):
        match = re.search(r"Port mode is (\S+)", value, re.M)
        if match:
            return match.group(1)

    def parse_duplex(self, value, intf_type="ethernet"):
        match = re.search(r"(\S+)-duplex", value, re.M)
        if match:
            return match.group(1)

    def parse_speed(self, value, intf_type="ethernet"):
        match = re.search(r"duplex, (.+)$", value, re.M)
        if match:
            return match.group(1)

    def parse_ipv4_address(self, value, intf_type="ethernet"):
        ipv4 = {}
        match = re.search(r"Internet Address is (.+)$", value, re.M)
        if match:
            address = match.group(1)
            addr = address.split("/")[0]
            ipv4["address"] = address.split("/")[0]
            ipv4["masklen"] = address.split("/")[1]
            self.facts["all_ipv4_addresses"].append(addr)
        return ipv4

    def populate_neighbors(self, data):
        objects = dict()
        # if there are no neighbors the show command returns
        # ERROR: No neighbour information
        if data.startswith("ERROR"):
            return dict()

        regex = re.compile(r"(\S+)\s+(\S+)\s+\d+\s+\w+\s+(\S+)")

        for item in data.split("\n")[4:-1]:
            match = regex.match(item)
            if match:
                nbor = dict()
                nbor["host"] = nbor["sysname"] = match.group(1)
                nbor["port"] = match.group(3)
                local_intf = normalize_interface(match.group(2))
                if local_intf not in objects:
                    objects[local_intf] = []
                objects[local_intf].append(nbor)

        return objects

    def populate_neighbors_cdp(self, data):
        facts = dict()

        for item in data.split("----------------------------------------"):
            if item == "":
                continue
            local_intf = self.parse_lldp_intf(item)
            if local_intf not in facts:
                facts[local_intf] = list()

            fact = dict()
            fact["port"] = self.parse_lldp_port(item)
            fact["sysname"] = self.parse_lldp_sysname(item)
            facts[local_intf].append(fact)

        return facts

    def parse_lldp_intf(self, data):
        match = re.search(r"Interface:\s*(\S+)", data, re.M)
        if match:
            return match.group(1).strip(",")

    def parse_lldp_port(self, data):
        match = re.search(r"Port ID \(outgoing port\):\s*(\S+)", data, re.M)
        if match:
            return match.group(1)

    def parse_lldp_sysname(self, data):
        match = re.search(r"Device ID:(.+)$", data, re.M)
        if match:
            return match.group(1)

    def populate_ipv6_interfaces(self, interfaces):
        facts = dict()
        for key, value in iteritems(interfaces):
            intf = dict()
            intf["ipv6"] = self.parse_ipv6_address(value)
            facts[key] = intf

    def parse_ipv6_address(self, value):
        ipv6 = {}
        match_addr = re.search(r"IPv6 address:\s*(\S+)", value, re.M)
        if match_addr:
            addr = match_addr.group(1)
            ipv6["address"] = addr
            self.facts["all_ipv6_addresses"].append(addr)
        match_subnet = re.search(r"IPv6 subnet:\s*(\S+)", value, re.M)
        if match_subnet:
            ipv6["subnet"] = match_subnet.group(1)

        return ipv6


class Legacy(FactsBase):
    # facts from nxos_facts 2.1

    VERSION_MAP = frozenset(
        [
            ("host_name", "_hostname"),
            ("kickstart_ver_str", "_os"),
            ("chassis_id", "_platform"),
        ],
    )

    MODULE_MAP = frozenset(
        [
            ("model", "model"),
            ("modtype", "type"),
            ("ports", "ports"),
            ("status", "status"),
        ],
    )

    FAN_MAP = frozenset(
        [
            ("fanname", "name"),
            ("fanmodel", "model"),
            ("fanhwver", "hw_ver"),
            ("fandir", "direction"),
            ("fanstatus", "status"),
        ],
    )

    POWERSUP_MAP = frozenset(
        [
            ("psmodel", "model"),
            ("psnum", "number"),
            ("ps_status", "status"),
            ("ps_status_3k", "status"),
            ("actual_out", "actual_output"),
            ("actual_in", "actual_in"),
            ("total_capa", "total_capacity"),
            ("input_type", "input_type"),
            ("watts", "watts"),
            ("amps", "amps"),
        ],
    )

    def populate(self):
        data = None

        data = self.run("show version", output="json")
        if data:
            if isinstance(data, dict):
                self.facts.update(self.transform_dict(data, self.VERSION_MAP))
            else:
                self.facts["hostname"] = self.parse_hostname(data)
                self.facts["os"] = self.parse_os(data)
                self.facts["platform"] = self.parse_platform(data)

        data = self.run("show interface", output="json")
        if data:
            if isinstance(data, dict):
                self.facts["interfaces_list"] = self.parse_structured_interfaces(data)
            else:
                self.facts["interfaces_list"] = self.parse_interfaces(data)

        data = self.run("show vlan brief", output="json")
        if data:
            if isinstance(data, dict):
                self.facts["vlan_list"] = self.parse_structured_vlans(data)
            else:
                self.facts["vlan_list"] = self.parse_vlans(data)

        data = self.run("show module", output="json")
        if data:
            if isinstance(data, dict):
                self.facts["module"] = self.parse_structured_module(data)
            else:
                self.facts["module"] = self.parse_module(data)

        data = self.run("show environment fan", output="json")
        if data:
            if isinstance(data, dict):
                self.facts["fan_info"] = self.parse_structured_fan_info(data)
            else:
                self.facts["fan_info"] = self.parse_fan_info(data)

        data = self.run("show environment power", output="json")
        if data:
            if isinstance(data, dict):
                self.facts["power_supply_info"] = self.parse_structured_power_supply_info(data)
            else:
                self.facts["power_supply_info"] = self.parse_power_supply_info(data)

    def parse_structured_interfaces(self, data):
        objects = list()
        data = data["TABLE_interface"]["ROW_interface"]
        if isinstance(data, dict):
            objects.append(data["interface"])
        elif isinstance(data, list):
            for item in data:
                objects.append(item["interface"])
        return objects

    def parse_structured_vlans(self, data):
        objects = list()
        data = data["TABLE_vlanbriefxbrief"]["ROW_vlanbriefxbrief"]
        if isinstance(data, dict):
            objects.append(data["vlanshowbr-vlanid-utf"])
        elif isinstance(data, list):
            for item in data:
                objects.append(item["vlanshowbr-vlanid-utf"])
        return objects

    def parse_structured_module(self, data):
        modinfo = data["TABLE_modinfo"]
        if isinstance(modinfo, dict):
            modinfo = [modinfo]

        objects = []
        for entry in modinfo:
            entry = entry["ROW_modinfo"]
            if isinstance(entry, dict):
                entry = [entry]
            entry_objects = list(self.transform_iterable(entry, self.MODULE_MAP))
            objects.extend(entry_objects)
        return objects

    def parse_structured_fan_info(self, data):
        objects = list()

        for key in ("fandetails", "fandetails_3k"):
            if data.get(key):
                try:
                    data = data[key]["TABLE_faninfo"]["ROW_faninfo"]
                except KeyError:
                    # Some virtual images don't actually report faninfo. In this case, move on and
                    # just return an empty list.
                    pass
                else:
                    objects = list(self.transform_iterable(data, self.FAN_MAP))
                break

        return objects

    def parse_structured_power_supply_info(self, data):
        ps_data = data.get("powersup", {})
        if ps_data.get("TABLE_psinfo_n3k"):
            fact = ps_data["TABLE_psinfo_n3k"]["ROW_psinfo_n3k"]
        else:
            # {TABLE,ROW}_psinfo keys have been renamed to
            # {TABLE,ROW}_ps_info in later NX-OS releases
            tab_key, row_key = "TABLE_psinfo", "ROW_psinfo"
            if tab_key not in ps_data:
                tab_key, row_key = "TABLE_ps_info", "ROW_ps_info"

            ps_tab_data = ps_data[tab_key]

            if isinstance(ps_tab_data, list):
                fact = []
                for i in ps_tab_data:
                    fact.append(i[row_key])
            else:
                fact = ps_tab_data[row_key]

        objects = list(self.transform_iterable(fact, self.POWERSUP_MAP))
        return objects

    def parse_hostname(self, data):
        match = re.search(r"\s+Device name:\s+(\S+)", data, re.M)
        if match:
            return match.group(1)

    def parse_os(self, data):
        match = re.search(r"\s+system:\s+version\s*(\S+)", data, re.M)
        if match:
            return match.group(1)
        else:
            match = re.search(r"\s+kickstart:\s+version\s*(\S+)", data, re.M)
            if match:
                return match.group(1)

    def parse_platform(self, data):
        match = re.search(r"Hardware\n\s+cisco\s+(\S+\s+\S+)", data, re.M)
        if match:
            return match.group(1)

    def parse_interfaces(self, data):
        objects = list()
        for line in data.split("\n"):
            if len(line) == 0:
                continue
            elif line.startswith("admin") or line[0] == " ":
                continue
            else:
                match = re.match(r"^(\S+)", line)
                if match:
                    intf = match.group(1)
                    if get_interface_type(intf) != "unknown":
                        objects.append(intf)
        return objects

    def parse_vlans(self, data):
        objects = list()
        for line in data.splitlines():
            if line == "":
                continue
            if line[0].isdigit():
                vlan = line.split()[0]
                objects.append(vlan)
        return objects

    def parse_module(self, data):
        objects = list()
        for line in data.splitlines():
            if line == "":
                break
            if line[0].isdigit():
                obj = {}
                match_port = re.search(r"\d\s*(\d*)", line, re.M)
                if match_port:
                    obj["ports"] = match_port.group(1)

                match = re.search(r"\d\s*\d*\s*(.+)$", line, re.M)
                if match:
                    line = match.group(1).split("  ")
                    items = list()
                    for item in line:
                        if item == "":
                            continue
                        items.append(item.strip())

                    if items:
                        obj["type"] = items[0]
                        obj["model"] = items[1]
                        obj["status"] = items[2]

                objects.append(obj)
        return objects

    def parse_fan_info(self, data):
        objects = list()

        for line in data.splitlines():
            if "-----------------" in line or "Status" in line:
                continue
            split_line = line.split()
            if len(split_line) > 1:
                obj = {}
                obj["name"] = split_line[0]
                obj["model"] = split_line[1]
                obj["hw_ver"] = split_line[-2]
                obj["status"] = split_line[-1]
                objects.append(obj)
        return objects

    def parse_power_supply_info(self, data):
        objects = list()

        for line in data.splitlines():
            if line == "":
                break
            if line[0].isdigit():
                obj = {}
                line = line.split()
                obj["model"] = line[1]
                obj["number"] = line[0]
                obj["status"] = line[-1]

                objects.append(obj)
        return objects
