"""
NET Finder
==========

.. versionadded:: 2017.7.0

A runner to find network details easily and fast.
It's smart enough to know what you are looking for.

Configuration
-------------

- Minion (proxy) config

    To have the complete features, one needs to add the following mine configuration in the minion (proxy) config file:

    .. code-block:: yaml

        mine_functions:
          net.ipaddrs: []
          net.lldp: []
          net.mac: []
          net.arp: []
          net.interfaces: []

    Which instructs Salt to cache the data returned by the NAPALM-functions.
    While they are not mandatory, the less functions configured, the less details will be found by the runner.

    How often the mines are refreshed, can be specified using:

    .. code-block:: yaml

        mine_interval: <X minutes>

- Master config

    By default the following options can be configured on the master.
    They are not necessary, but available in case the user has different requirements.

    target: ``*``
        From what minions will collect the mine data. Default: ``*`` (collect from all minions).

    expr_form: ``glob``
        Minion matching expression form. Default: ``glob``.

    ignore_interfaces
        A list of interfaces name to ignore. By default will consider all interfaces.

    display: ``True``
        Display on the screen or return structured object? Default: ``True`` (return on the CLI).

    outputter: ``table``
        Specify the outputter name when displaying on the CLI. Default: :mod:`table <salt.output.napalm_bgp>`.

    Configuration example:

    .. code-block:: yaml

        runners:
          net.find:
            target: 'edge*'
            expr_form: 'glob'
            ignore_interfaces:
              - lo0
              - em1
              - jsrv
              - fxp0
            outputter: yaml
"""

import salt.output
import salt.utils.network

try:
    # pylint: disable=no-name-in-module
    from napalm.base import helpers as napalm_helpers
    from netaddr import IPNetwork  # netaddr is already required by napalm
    from netaddr.core import AddrFormatError

    # pylint: enable=no-name-in-module

    HAS_NAPALM = True
except ImportError:
    HAS_NAPALM = False

# -----------------------------------------------------------------------------
# module properties
# -----------------------------------------------------------------------------

_DEFAULT_TARGET = "*"
_DEFAULT_EXPR_FORM = "glob"
_DEFAULT_IGNORE_INTF = []
# 'lo0', 'em1', 'em0', 'jsrv', 'fxp0'
_DEFAULT_DISPLAY = True
_DEFAULT_OUTPUTTER = "table"


# -----------------------------------------------------------------------------
# global variables
# -----------------------------------------------------------------------------

# will cache several details to avoid loading them several times from the mines.
_CACHE = {}

# -----------------------------------------------------------------------------
# helper functions -- will not be exported
# -----------------------------------------------------------------------------

# Define the module's virtual name
__virtualname__ = "net"


def __virtual__():
    if HAS_NAPALM:
        return __virtualname__
    return (False, "The napalm module could not be imported")


def _get_net_runner_opts():
    """
    Return the net.find runner options.
    """
    runner_opts = __opts__.get("runners", {}).get("net.find", {})
    return {
        "target": runner_opts.get("target", _DEFAULT_TARGET),
        "expr_form": runner_opts.get("expr_form", _DEFAULT_EXPR_FORM),
        "ignore_interfaces": runner_opts.get("ignore_interfaces", _DEFAULT_IGNORE_INTF),
        "display": runner_opts.get("display", _DEFAULT_DISPLAY),
        "outputter": runner_opts.get("outputter", _DEFAULT_OUTPUTTER),
    }


def _get_mine(fun):
    """
    Return the mine function from all the targeted minions.
    Just a small helper to avoid redundant pieces of code.
    """
    if fun in _CACHE and _CACHE[fun]:
        return _CACHE[fun]
    net_runner_opts = _get_net_runner_opts()
    _CACHE[fun] = __salt__["mine.get"](
        net_runner_opts.get("target"), fun, tgt_type=net_runner_opts.get("expr_form")
    )
    return _CACHE[fun]


def _display_runner(rows, labels, title, display=_DEFAULT_DISPLAY):
    """
    Display or return the rows.
    """
    if display:
        net_runner_opts = _get_net_runner_opts()
        if net_runner_opts.get("outputter") == "table":
            ret = salt.output.out_format(
                {"rows": rows, "labels": labels},
                "table",
                __opts__,
                title=title,
                rows_key="rows",
                labels_key="labels",
            )
        else:
            ret = salt.output.out_format(
                rows, net_runner_opts.get("outputter"), __opts__
            )
        print(ret)
    else:
        return rows


def _get_network_obj(addr):
    """
    Try to convert a string into a valid IP Network object.
    """
    ip_netw = None
    try:
        ip_netw = IPNetwork(addr)
    except AddrFormatError:
        return ip_netw
    return ip_netw


def _find_interfaces_ip(mac):
    """
    Helper to search the interfaces IPs using the MAC address.
    """
    try:
        mac = napalm_helpers.convert(napalm_helpers.mac, mac)
    except AddrFormatError:
        return ("", "", [])

    all_interfaces = _get_mine("net.interfaces")
    all_ipaddrs = _get_mine("net.ipaddrs")

    for device, device_interfaces in all_interfaces.items():
        if not device_interfaces.get("result", False):
            continue
        for interface, interface_details in device_interfaces.get("out", {}).items():
            try:
                interface_mac = napalm_helpers.convert(
                    napalm_helpers.mac, interface_details.get("mac_address")
                )
            except AddrFormatError:
                continue
            if mac != interface_mac:
                continue
            interface_ipaddrs = (
                all_ipaddrs.get(device, {}).get("out", {}).get(interface, {})
            )
            ip_addresses = interface_ipaddrs.get("ipv4", {})
            ip_addresses.update(interface_ipaddrs.get("ipv6", {}))
            interface_ips = [
                "{}/{}".format(ip_addr, addr_details.get("prefix_length", "32"))
                for ip_addr, addr_details in ip_addresses.items()
            ]
            return device, interface, interface_ips

    return ("", "", [])


def _find_interfaces_mac(ip):  # pylint: disable=invalid-name
    """
    Helper to get the interfaces hardware address using the IP Address.
    """
    all_interfaces = _get_mine("net.interfaces")
    all_ipaddrs = _get_mine("net.ipaddrs")

    for device, device_ipaddrs in all_ipaddrs.items():
        if not device_ipaddrs.get("result", False):
            continue
        for interface, interface_ipaddrs in device_ipaddrs.get("out", {}).items():
            ip_addresses = set(interface_ipaddrs.get("ipv4", {}).keys())
            ip_addresses.update(set(interface_ipaddrs.get("ipv6", {}).keys()))
            for ipaddr in ip_addresses:
                if ip != ipaddr:
                    continue
                interface_mac = (
                    all_interfaces.get(device, {})
                    .get("out", {})
                    .get(interface, {})
                    .get("mac_address", "")
                )
                return device, interface, interface_mac

    return ("", "", "")


# -----------------------------------------------------------------------------
# callable functions
# -----------------------------------------------------------------------------


def interfaces(
    device=None,
    interface=None,
    title=None,
    pattern=None,
    ipnet=None,
    best=True,
    display=_DEFAULT_DISPLAY,
):
    """
    Search for interfaces details in the following mine functions:

    - net.interfaces
    - net.ipaddrs

    Optional arguments:

    device
        Return interface data from a certain device only.

    interface
        Return data selecting by interface name.

    pattern
        Return interfaces that contain a certain pattern in their description.

    ipnet
        Return interfaces whose IP networks associated include this IP network.

    best: ``True``
        When ``ipnet`` is specified, this argument says if the runner should return only the best match
        (the output will contain at most one row). Default: ``True`` (return only the best match).

    display: True
        Display on the screen or return structured object? Default: ``True`` (return on the CLI).

    title
        Display a custom title for the table.

    CLI Example:

    .. code-block:: bash

        $ sudo salt-run net.interfaces interface=vt-0/0/10

    Output Example:

    .. code-block:: text

        Details for interface xe-0/0/0
        _________________________________________________________________________________________________________________
        |    Device    | Interface | Interface Description |  UP  | Enabled | Speed [Mbps] | MAC Address | IP Addresses |
        _________________________________________________________________________________________________________________
        | edge01.bjm01 | vt-0/0/10 |                       | True |   True  |     1000     |             |              |
        _________________________________________________________________________________________________________________
        | edge01.flw01 | vt-0/0/10 |                       | True |   True  |     1000     |             |              |
        _________________________________________________________________________________________________________________
        | edge01.pos01 | vt-0/0/10 |                       | True |   True  |     1000     |             |              |
        _________________________________________________________________________________________________________________
        | edge01.oua01 | vt-0/0/10 |                       | True |   True  |     1000     |             |              |
        _________________________________________________________________________________________________________________
    """

    def _ipnet_belongs(net):
        """
        Helper to tell if a IP address or network belong to a certain network.
        """
        if net == "0.0.0.0/0":
            return False
        net_obj = _get_network_obj(net)
        if not net_obj:
            return False
        return ipnet in net_obj or net_obj in ipnet

    labels = {
        "device": "Device",
        "interface": "Interface",
        "interface_description": "Interface Description",
        "is_up": "UP",
        "is_enabled": "Enabled",
        "speed": "Speed [Mbps]",
        "mac": "MAC Address",
        "ips": "IP Addresses",
    }
    rows = []

    net_runner_opts = _get_net_runner_opts()

    if pattern:
        title = (
            'Pattern "{}" found in the description of the following interfaces'.format(
                pattern
            )
        )
    if not title:
        title = "Details"
        if interface:
            title += f" for interface {interface}"
        else:
            title += " for all interfaces"
        if device:
            title += f" on device {device}"
        if ipnet:
            title += f" that include network {str(ipnet)}"
            if best:
                title += " - only best match returned"

    all_interfaces = _get_mine("net.interfaces")
    all_ipaddrs = _get_mine("net.ipaddrs")

    if device:
        all_interfaces = {device: all_interfaces.get(device, {})}

    if ipnet and not isinstance(ipnet, IPNetwork):
        ipnet = _get_network_obj(ipnet)

    best_row = {}
    best_net_match = IPNetwork("0.0.0.0/0")
    for (
        device,
        net_interfaces_out,
    ) in all_interfaces.items():  # pylint: disable=too-many-nested-blocks
        if not net_interfaces_out:
            continue
        if not net_interfaces_out.get("result", False):
            continue
        selected_device_interfaces = net_interfaces_out.get("out", {})
        if interface:
            selected_device_interfaces = {
                interface: selected_device_interfaces.get(interface, {})
            }
        for interface_name, interface_details in selected_device_interfaces.items():
            if not interface_details:
                continue
            if ipnet and interface_name in net_runner_opts.get("ignore_interfaces"):
                continue
            interface_description = interface_details.get("description", "") or ""
            if pattern:
                if pattern.lower() not in interface_description.lower():
                    continue
            if not all_ipaddrs.get(device, {}).get("result", False):
                continue
            ips = []
            device_entry = {
                "device": device,
                "interface": interface_name,
                "interface_description": interface_description,
                "is_up": (interface_details.get("is_up", "") or ""),
                "is_enabled": (interface_details.get("is_enabled", "") or ""),
                "speed": (interface_details.get("speed", "") or ""),
                "mac": napalm_helpers.convert(
                    napalm_helpers.mac, (interface_details.get("mac_address", "") or "")
                ),
                "ips": [],
            }
            intf_entry_found = False
            for intrf, interface_ips in (
                all_ipaddrs.get(device, {}).get("out", {}).items()
            ):
                if intrf.split(".")[0] == interface_name:
                    ip_addresses = interface_ips.get("ipv4", {})  # all IPv4 addresses
                    ip_addresses.update(
                        interface_ips.get("ipv6", {})
                    )  # and all IPv6 addresses
                    ips = [
                        "{}/{}".format(ip_addr, addr_details.get("prefix_length", "32"))
                        for ip_addr, addr_details in ip_addresses.items()
                    ]
                    interf_entry = {}
                    interf_entry.update(device_entry)
                    interf_entry["ips"] = ips
                    if display:
                        interf_entry["ips"] = "\n".join(interf_entry["ips"])
                    if ipnet:
                        inet_ips = [
                            str(ip) for ip in ips if _ipnet_belongs(ip)
                        ]  # filter and get only IP include ipnet
                        if inet_ips:  # if any
                            if best:
                                # determine the global best match
                                compare = [best_net_match]
                                compare.extend(list(map(_get_network_obj, inet_ips)))
                                new_best_net_match = max(compare)
                                if new_best_net_match != best_net_match:
                                    best_net_match = new_best_net_match
                                    best_row = interf_entry
                            else:
                                # or include all
                                intf_entry_found = True
                                rows.append(interf_entry)
                    else:
                        intf_entry_found = True
                        rows.append(interf_entry)
            if not intf_entry_found and not ipnet:
                interf_entry = {}
                interf_entry.update(device_entry)
                if display:
                    interf_entry["ips"] = ""
                rows.append(interf_entry)

    if ipnet and best and best_row:
        rows = [best_row]

    return _display_runner(rows, labels, title, display=display)


def findarp(
    device=None, interface=None, mac=None, ip=None, display=_DEFAULT_DISPLAY
):  # pylint: disable=invalid-name
    """
    Search for entries in the ARP tables using the following mine functions:

    - net.arp

    Optional arguments:

    device
        Return interface data from a certain device only.

    interface
        Return data selecting by interface name.

    mac
        Search using a specific MAC Address.

    ip
        Search using a specific IP Address.

    display: ``True``
        Display on the screen or return structured object? Default: ``True``, will return on the CLI.

    CLI Example:

    .. code-block:: bash

        $ sudo salt-run net.findarp mac=8C:60:0F:78:EC:41

    Output Example:

    .. code-block:: text

        ARP Entries for MAC 8C:60:0F:78:EC:41
        ________________________________________________________________________________
        |    Device    |     Interface     |        MAC        |       IP      |  Age  |
        ________________________________________________________________________________
        | edge01.bjm01 | irb.171 [ae0.171] | 8C:60:0F:78:EC:41 | 172.172.17.19 | 956.0 |
        ________________________________________________________________________________
    """
    labels = {
        "device": "Device",
        "interface": "Interface",
        "mac": "MAC",
        "ip": "IP",
        "age": "Age",
    }
    rows = []

    all_arp = _get_mine("net.arp")

    title = "ARP Entries"
    if device:
        title += f" on device {device}"
    if interface:
        title += f" on interface {interface}"
    if ip:
        title += f" for IP {ip}"
    if mac:
        title += f" for MAC {mac}"

    if device:
        all_arp = {device: all_arp.get(device)}

    for device, device_arp in all_arp.items():
        if not device_arp:
            continue
        if not device_arp.get("result", False):
            continue
        arp_table = device_arp.get("out", [])
        for arp_entry in arp_table:
            if (
                (mac and arp_entry.get("mac", "").lower() == mac.lower())
                or (  # pylint: disable=too-many-boolean-expressions
                    interface and interface in arp_entry.get("interface", "")
                )
                or (
                    ip
                    and napalm_helpers.convert(
                        napalm_helpers.ip, arp_entry.get("ip", "")
                    )
                    == napalm_helpers.convert(napalm_helpers.ip, ip)
                )
            ):
                rows.append(
                    {
                        "device": device,
                        "interface": arp_entry.get("interface"),
                        "mac": napalm_helpers.convert(
                            napalm_helpers.mac, arp_entry.get("mac")
                        ),
                        "ip": napalm_helpers.convert(
                            napalm_helpers.ip, arp_entry.get("ip")
                        ),
                        "age": arp_entry.get("age"),
                    }
                )

    return _display_runner(rows, labels, title, display=display)


def findmac(device=None, mac=None, interface=None, vlan=None, display=_DEFAULT_DISPLAY):
    """
    Search in the MAC Address tables, using the following mine functions:

    - net.mac

    Optional arguments:

    device
        Return interface data from a certain device only.

    interface
        Return data selecting by interface name.

    mac
        Search using a specific MAC Address.

    vlan
        Search using a VLAN ID.

    display: ``True``
        Display on the screen or return structured object? Default: ``True``, will return on the CLI.

    CLI Example:

    .. code-block:: bash

        $ sudo salt-run net.findmac mac=8C:60:0F:78:EC:41

    Output Example:

    .. code-block:: text

        MAC Address(es)
        _____________________________________________________________________________________________
        |    Device    | Interface |        MAC        | VLAN | Static | Active | Moves | Last move |
        _____________________________________________________________________________________________
        | edge01.bjm01 |  ae0.171  | 8C:60:0F:78:EC:41 | 171  | False  |  True  |   0   |    0.0    |
        _____________________________________________________________________________________________
    """
    labels = {
        "device": "Device",
        "interface": "Interface",
        "mac": "MAC",
        "vlan": "VLAN",
        "static": "Static",
        "active": "Active",
        "moves": "Moves",
        "last_move": "Last Move",
    }
    rows = []

    all_mac = _get_mine("net.mac")

    title = "MAC Address(es)"
    if device:
        title += f" on device {device}"
    if interface:
        title += f" on interface {interface}"
    if vlan:
        title += f" on VLAN {vlan}"

    if device:
        all_mac = {device: all_mac.get(device)}

    for device, device_mac in all_mac.items():
        if not device_mac:
            continue
        if not device_mac.get("result", False):
            continue
        mac_table = device_mac.get("out", [])
        for mac_entry in mac_table:
            if (
                (
                    mac
                    and napalm_helpers.convert(  # pylint: disable=too-many-boolean-expressions
                        napalm_helpers.mac, mac_entry.get("mac", "")
                    )
                    == napalm_helpers.convert(napalm_helpers.mac, mac)
                )
                or (interface and interface in mac_entry.get("interface", ""))
                or (vlan and str(mac_entry.get("vlan", "")) == str(vlan))
            ):
                rows.append(
                    {
                        "device": device,
                        "interface": mac_entry.get("interface"),
                        "mac": napalm_helpers.convert(
                            napalm_helpers.mac, mac_entry.get("mac")
                        ),
                        "vlan": mac_entry.get("vlan"),
                        "static": mac_entry.get("static"),
                        "active": mac_entry.get("active"),
                        "moves": mac_entry.get("moves"),
                        "last_move": mac_entry.get("last_move"),
                    }
                )

    return _display_runner(rows, labels, title, display=display)


def lldp(
    device=None,
    interface=None,
    title=None,
    pattern=None,
    chassis=None,
    display=_DEFAULT_DISPLAY,
):
    """
    Search in the LLDP neighbors, using the following mine functions:

    - net.lldp

    Optional arguments:

    device
        Return interface data from a certain device only.

    interface
        Return data selecting by interface name.

    pattern
        Return LLDP neighbors that have contain this pattern in one of the following fields:

        - Remote Port ID
        - Remote Port Description
        - Remote System Name
        - Remote System Description

    chassis
        Search using a specific Chassis ID.

    display: ``True``
        Display on the screen or return structured object? Default: ``True`` (return on the CLI).

    display: ``True``
        Display on the screen or return structured object? Default: ``True`` (return on the CLI).

    title
        Display a custom title for the table.

    CLI Example:

    .. code-block:: bash

        $ sudo salt-run net.lldp pattern=Ethernet1/48

    Output Example:

    .. code-block:: text

        Pattern "Ethernet1/48" found in one of the following LLDP details
        _________________________________________________________________________________________________________________________________________________________________________________________
        |    Device    | Interface | Parent Interface | Remote Chassis ID | Remote Port ID | Remote Port Description |   Remote System Name   |            Remote System Description            |
        _________________________________________________________________________________________________________________________________________________________________________________________
        | edge01.bjm01 |  xe-2/3/4 |       ae0        | 8C:60:4F:3B:52:19 |                |       Ethernet1/48      | edge05.bjm01.dummy.net |   Cisco NX-OS(tm) n6000, Software (n6000-uk9),  |
        |              |           |                  |                   |                |                         |                        | Version 7.3(0)N7(5), RELEASE SOFTWARE Copyright |
        |              |           |                  |                   |                |                         |                        |  (c) 2002-2012 by Cisco Systems, Inc. Compiled  |
        |              |           |                  |                   |                |                         |                        |                2/17/2016 22:00:00               |
        _________________________________________________________________________________________________________________________________________________________________________________________
        | edge01.flw01 |  xe-1/2/3 |       ae0        | 8C:60:4F:1A:B4:22 |                |       Ethernet1/48      | edge05.flw01.dummy.net |   Cisco NX-OS(tm) n6000, Software (n6000-uk9),  |
        |              |           |                  |                   |                |                         |                        | Version 7.3(0)N7(5), RELEASE SOFTWARE Copyright |
        |              |           |                  |                   |                |                         |                        |  (c) 2002-2012 by Cisco Systems, Inc. Compiled  |
        |              |           |                  |                   |                |                         |                        |                2/17/2016 22:00:00               |
        _________________________________________________________________________________________________________________________________________________________________________________________
        | edge01.oua01 |  xe-0/1/2 |       ae1        | 8C:60:4F:51:A4:22 |                |       Ethernet1/48      | edge05.oua01.dummy.net |   Cisco NX-OS(tm) n6000, Software (n6000-uk9),  |
        |              |           |                  |                   |                |                         |                        | Version 7.3(0)N7(5), RELEASE SOFTWARE Copyright |
        |              |           |                  |                   |                |                         |                        |  (c) 2002-2012 by Cisco Systems, Inc. Compiled  |
        |              |           |                  |                   |                |                         |                        |                2/17/2016 22:00:00               |
        _________________________________________________________________________________________________________________________________________________________________________________________
    """
    all_lldp = _get_mine("net.lldp")

    labels = {
        "device": "Device",
        "interface": "Interface",
        "parent_interface": "Parent Interface",
        "remote_chassis_id": "Remote Chassis ID",
        "remote_port_id": "Remote Port ID",
        "remote_port_desc": "Remote Port Description",
        "remote_system_name": "Remote System Name",
        "remote_system_desc": "Remote System Description",
    }
    rows = []

    if pattern:
        title = 'Pattern "{}" found in one of the following LLDP details'.format(
            pattern
        )
    if not title:
        title = "LLDP Neighbors"
        if interface:
            title += f" for interface {interface}"
        else:
            title += " for all interfaces"
        if device:
            title += f" on device {device}"
        if chassis:
            title += f" having Chassis ID {chassis}"

    if device:
        all_lldp = {device: all_lldp.get(device)}

    for device, device_lldp in all_lldp.items():
        if not device_lldp:
            continue
        if not device_lldp.get("result", False):
            continue
        lldp_interfaces = device_lldp.get("out", {})
        if interface:
            lldp_interfaces = {interface: lldp_interfaces.get(interface, [])}
        for intrf, interface_lldp in lldp_interfaces.items():
            if not interface_lldp:
                continue
            for lldp_row in interface_lldp:
                rsn = lldp_row.get("remote_system_name", "") or ""
                rpi = lldp_row.get("remote_port_id", "") or ""
                rsd = lldp_row.get("remote_system_description", "") or ""
                rpd = lldp_row.get("remote_port_description", "") or ""
                rci = lldp_row.get("remote_chassis_id", "") or ""
                if pattern:
                    ptl = pattern.lower()
                    if not (
                        (ptl in rsn.lower())
                        or (ptl in rsd.lower())
                        or (ptl in rpd.lower())
                        or (ptl in rci.lower())
                    ):
                        # nothing matched, let's move on
                        continue
                if chassis:
                    if napalm_helpers.convert(
                        napalm_helpers.mac, rci
                    ) != napalm_helpers.convert(napalm_helpers.mac, chassis):
                        continue
                rows.append(
                    {
                        "device": device,
                        "interface": intrf,
                        "parent_interface": (
                            lldp_row.get("parent_interface", "") or ""
                        ),
                        "remote_chassis_id": napalm_helpers.convert(
                            napalm_helpers.mac, rci
                        ),
                        "remote_port_id": rpi,
                        "remote_port_descr": rpd,
                        "remote_system_name": rsn,
                        "remote_system_descr": rsd,
                    }
                )

    return _display_runner(rows, labels, title, display=display)


def find(addr, best=True, display=_DEFAULT_DISPLAY):
    """
    Search in all possible entities (Interfaces, MAC tables, ARP tables, LLDP neighbors),
    using the following mine functions:

    - net.mac
    - net.arp
    - net.lldp
    - net.ipaddrs
    - net.interfaces

    This function has the advantage that it knows where to look, but the output might
    become quite long as returns all possible matches.

    Optional arguments:

    best: ``True``
        Return only the best match with the interfaces IP networks
        when the saerching pattern is a valid IP Address or Network.

    display: ``True``
        Display on the screen or return structured object? Default: ``True`` (return on the CLI).

    CLI Example:

    .. code-block:: bash

        $ sudo salt-run net.find 10.10.10.7

    Output Example:

    .. code-block:: text

        Details for all interfaces that include network 10.10.10.7/32 - only best match returned
        ________________________________________________________________________________________________________________________
        |    Device    | Interface | Interface Description |  UP  | Enabled | Speed [Mbps] |    MAC Address    |  IP Addresses |
        ________________________________________________________________________________________________________________________
        | edge01.flw01 |    irb    |                       | True |   True  |      -1      | 5C:5E:AB:AC:52:B4 | 10.10.10.1/22 |
        ________________________________________________________________________________________________________________________

        ARP Entries for IP 10.10.10.7
        _____________________________________________________________________________
        |    Device    |     Interface     |        MAC        |     IP     |  Age  |
        _____________________________________________________________________________
        | edge01.flw01 | irb.349 [ae0.349] | 2C:60:0C:2A:4C:0A | 10.10.10.7 | 832.0 |
        _____________________________________________________________________________
    """
    if not addr:
        if display:
            print("Please type a valid MAC/IP Address / Device / Interface / VLAN")
        return {}

    device = ""
    interface = ""
    mac = ""
    ip = ""  # pylint: disable=invalid-name
    ipnet = None

    results = {
        "int_net": [],
        "int_descr": [],
        "int_name": [],
        "int_ip": [],
        "int_mac": [],
        "int_device": [],
        "lldp_descr": [],
        "lldp_int": [],
        "lldp_device": [],
        "lldp_mac": [],
        "lldp_device_int": [],
        "mac_device": [],
        "mac_int": [],
        "arp_device": [],
        "arp_int": [],
        "arp_mac": [],
        "arp_ip": [],
    }

    if isinstance(addr, int):
        results["mac"] = findmac(vlan=addr, display=display)
        if not display:
            return results
        else:
            return None

    try:
        mac = napalm_helpers.convert(napalm_helpers.mac, addr)
    except IndexError:
        # no problem, let's keep searching
        pass
    if salt.utils.network.is_ipv6(addr):
        mac = False
    if not mac:
        try:
            ip = napalm_helpers.convert(
                napalm_helpers.ip, addr
            )  # pylint: disable=invalid-name
        except ValueError:
            pass
        ipnet = _get_network_obj(addr)
        if ipnet:
            results["int_net"] = interfaces(ipnet=ipnet, best=best, display=display)
        if not (ipnet or ip):
            # search in all possible places
            # display all interfaces details
            results["int_descr"] = interfaces(pattern=addr, display=display)
            results["int_name"] = interfaces(interface=addr, display=display)
            results["int_device"] = interfaces(device=addr, display=display)
            # search in LLDP details
            results["lldp_descr"] = lldp(pattern=addr, display=display)
            results["lldp_int"] = lldp(interface=addr, display=display)
            results["lldp_device"] = lldp(device=addr, display=display)
            # search in MAC Address tables
            results["mac_device"] = findmac(device=addr, display=display)
            results["mac_int"] = findmac(interface=addr, display=display)
            # search in ARP tables
            results["arp_device"] = findarp(device=addr, display=display)
            results["arp_int"] = findarp(interface=addr, display=display)
            if not display:
                return results
    if mac:
        results["int_descr"] = findmac(mac=mac, display=display)
        results["arp_mac"] = findarp(mac=mac, display=display)
        results["lldp_mac"] = lldp(chassis=mac, display=display)
    if ip:
        results["arp_ip"] = findarp(ip=ip, display=display)

    # let's search in Interfaces

    if mac:
        device, interface, ips = _find_interfaces_ip(mac)
        ip = ", ".join(ips)  # pylint: disable=invalid-name
        if device and interface:
            title = "Interface {interface} on {device} has the physical address ({mac})".format(
                interface=interface, device=device, mac=mac
            )
            results["int_mac"] = interfaces(
                device=device, interface=interface, title=title, display=display
            )

    elif ip:
        device, interface, mac = _find_interfaces_mac(ip)
        if device and interface:
            title = (
                "IP Address {ip} is set for interface {interface}, on {device}".format(
                    interface=interface, device=device, ip=ip
                )
            )
            results["int_ip"] = interfaces(
                device=device, interface=interface, title=title, display=display
            )

    if device and interface:
        results["lldp_device_int"] = lldp(device, interface, display=display)

    if not display:
        return results


def multi_find(*patterns, **kwargs):
    """
    Execute multiple search tasks.
    This function is based on the `find` function.
    Depending on the search items, some information might overlap.

    Optional arguments:

    best: ``True``
        Return only the best match with the interfaces IP networks
        when the saerching pattern is a valid IP Address or Network.

    display: ``True``
        Display on the screen or return structured object? Default: `True` (return on the CLI).

    CLI Example:

    .. code-block:: bash

        $ sudo salt-run net.multi_find Ethernet1/49 xe-0/1/2

    Output Example:

    .. code-block:: text

        Pattern "Ethernet1/49" found in one of the following LLDP details

            -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
            |    Device    | Interface | Parent Interface | Remote Chassis ID | Remote Port Description | Remote Port ID |          Remote System Description          |   Remote System Name   |
            -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
            | edge01.oua04 |  xe-0/1/2 |       ae1        | DE:AD:BE:EF:DE:AD |       Ethernet1/49      |                | Cisco NX-OS(tm) n6000, Software (n6000-uk9) | edge07.oua04.dummy.net |
            -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

        Details for interface xe-0/1/2

            -----------------------------------------------------------------------------------------------------------------------
            |    Device    | Interface | Interface Description | IP Addresses | Enabled |  UP  |    MAC Address    | Speed [Mbps] |
            -----------------------------------------------------------------------------------------------------------------------
            | edge01.oua04 |  xe-0/1/2 |     ae1 sw01.oua04    |              |   True  | True | BE:EF:DE:AD:BE:EF |    10000     |
            -----------------------------------------------------------------------------------------------------------------------

        LLDP Neighbors for interface xe-0/1/2

            -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
            |    Device    | Interface | Parent Interface | Remote Chassis ID | Remote Port Description | Remote Port ID |          Remote System Description          |   Remote System Name   |
            -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
            | edge01.oua04 |  xe-0/1/2 |       ae1        | DE:AD:BE:EF:DE:AD |       Ethernet1/49      |                | Cisco NX-OS(tm) n6000, Software (n6000-uk9) | edge07.oua04.dummy.net |
            -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    """
    out = {}
    for pattern in set(patterns):
        search_result = find(
            pattern,
            best=kwargs.get("best", True),
            display=kwargs.get("display", _DEFAULT_DISPLAY),
        )
        out[pattern] = search_result
    if not kwargs.get("display", _DEFAULT_DISPLAY):
        return out
