#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function

__metaclass__ = type

# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)


DOCUMENTATION = """
module: nxos_pim_interface
extends_documentation_fragment:
- cisco.nxos.nxos
short_description: Manages PIM interface configuration.
description:
- Manages PIM interface configuration settings.
version_added: 1.0.0
author:
- Jason Edelman (@jedelman8)
notes:
- Tested against NXOSv 7.3.(0)D1(1) on VIRL
- When C(state=default), supported params will be reset to a default state. These
  include C(dr_prio), C(hello_auth_key), C(hello_interval), C(jp_policy_out), C(jp_policy_in),
  C(jp_type_in), C(jp_type_out), C(border), C(neighbor_policy), C(neighbor_type).
- The C(hello_auth_key) param is not idempotent.
- C(hello_auth_key) only supports clear text passwords.
- When C(state=absent), pim interface configuration will be set to defaults and pim-sm
  will be disabled on the interface.
- PIM must be enabled on the device to use this module.
- This module is for Layer 3 interfaces.
options:
  interface:
    description:
    - Full name of the interface such as Ethernet1/33.
    type: str
    required: true
  sparse:
    description:
    - Enable/disable sparse-mode on the interface.
    type: bool
    default: false
  bfd:
    description:
    - Enables BFD for PIM at the interface level. This overrides the bfd variable
      set at the pim global level.
    - Valid values are 'enable', 'disable' or 'default'.
    - "Dependency: ''feature bfd''"
    type: str
    choices:
    - enable
    - disable
    - default
  dr_prio:
    description:
    - Configures priority for PIM DR election on interface.
    type: str
  hello_auth_key:
    description:
    - Authentication for hellos on this interface.
    type: str
  hello_interval:
    description:
    - Hello interval in milliseconds for this interface.
    type: int
  jp_policy_out:
    description:
    - Policy for join-prune messages (outbound).
    type: str
  jp_policy_in:
    description:
    - Policy for join-prune messages (inbound).
    type: str
  jp_type_out:
    description:
    - Type of policy mapped to C(jp_policy_out).
    type: str
    choices:
    - prefix
    - routemap
  jp_type_in:
    description:
    - Type of policy mapped to C(jp_policy_in).
    type: str
    choices:
    - prefix
    - routemap
  border:
    description:
    - Configures interface to be a boundary of a PIM domain.
    type: bool
    default: false
  neighbor_policy:
    description:
    - Configures a neighbor policy for filtering adjacencies.
    type: str
  neighbor_type:
    description:
    - Type of policy mapped to neighbor_policy.
    type: str
    choices:
    - prefix
    - routemap
  state:
    description:
    - Manages desired state of the resource.
    type: str
    choices:
    - present
    - default
    default: present
"""
EXAMPLES = """
- name: Ensure PIM is not running on the interface
  cisco.nxos.nxos_pim_interface:
    interface: eth1/33
    state: absent

- name: Ensure the interface has pim-sm enabled with the appropriate priority and
    hello interval
  cisco.nxos.nxos_pim_interface:
    interface: eth1/33
    dr_prio: 10
    hello_interval: 40
    state: present

- name: Ensure join-prune policies exist
  cisco.nxos.nxos_pim_interface:
    interface: eth1/33
    jp_policy_in: JPIN
    jp_policy_out: JPOUT
    jp_type_in: routemap
    jp_type_out: routemap

- name: disable bfd on the interface
  cisco.nxos.nxos_pim_interface:
    interface: eth1/33
    bfd: disable

- name: Ensure defaults are in place
  cisco.nxos.nxos_pim_interface:
    interface: eth1/33
    state: default
"""

RETURN = r"""
commands:
    description: command sent to the device
    returned: always
    type: list
    sample: ["interface eth1/33",
             "ip pim neighbor-policy test",
             "ip pim bfd-instance disable",
             "ip pim neighbor-policy test"
            ]
"""

import re
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import (
    get_config,
    load_config,
    run_commands,
)
from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import (
    nxos_argument_spec,
)
from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import (
    get_interface_type,
)


PARAM_TO_COMMAND_KEYMAP = {
    "interface": "",
    "bfd": "ip pim bfd-instance",
    "sparse": "ip pim sparse-mode",
    "dr_prio": "ip pim dr-priority {0}",
    "hello_interval": "ip pim hello-interval {0}",
    "hello_auth_key": "ip pim hello-authentication ah-md5 {0}",
    "border": "ip pim border",
    "jp_policy_out": "ip pim jp-policy prefix-list {0} out",
    "jp_policy_in": "ip pim jp-policy prefix-list {0} in",
    "jp_type_in": "",
    "jp_type_out": "",
    "neighbor_policy": "ip pim neighbor-policy prefix-list {0}",
    "neighbor_type": "",
}

PARAM_TO_DEFAULT_KEYMAP = {
    "bfd": "default",
    "dr_prio": "1",
    "hello_interval": "30000",
    "sparse": False,
    "border": False,
    "hello_auth_key": False,
}

BFD_KEYMAP = {
    None: None,
    "default": "no ip pim bfd-instance",
    "disable": "ip pim bfd-instance disable",
    "enable": "ip pim bfd-instance",
}


def execute_show_command(command, module, text=False):
    if text:
        cmds = [{"command": command, "output": "text"}]
    else:
        cmds = [{"command": command, "output": "json"}]

    return run_commands(module, cmds)


def flatten_list(command_lists):
    flat_command_list = []
    for command in command_lists:
        if isinstance(command, list):
            flat_command_list.extend(command)
        else:
            flat_command_list.append(command)
    return flat_command_list


def local_existing(gexisting):
    jp_bidir = False
    isauth = False
    if gexisting:
        jp_bidir = gexisting.get("jp_bidir")
        isauth = gexisting.get("isauth")
        if jp_bidir and isauth:
            gexisting.pop("jp_bidir")
            gexisting.pop("isauth")

    return gexisting, jp_bidir, isauth


def get_interface_mode(interface, intf_type, module):
    mode = "unknown"
    command = "show interface {0}".format(interface)
    body = execute_show_command(command, module)

    try:
        interface_table = body[0]["TABLE_interface"]["ROW_interface"]
    except (KeyError, AttributeError, IndexError):
        return mode

    if intf_type in ["ethernet", "portchannel"]:
        mode = str(interface_table.get("eth_mode", "layer3"))
        if mode in ["access", "trunk"]:
            mode = "layer2"
        elif mode == "routed":
            mode = "layer3"
    elif intf_type in ["loopback", "svi"]:
        mode = "layer3"
    return mode


def get_pim_interface(module, interface):
    pim_interface = {}
    body = get_config(module, flags=["interface {0}".format(interface)])

    pim_interface["bfd"] = "default"
    pim_interface["neighbor_type"] = None
    pim_interface["neighbor_policy"] = None
    pim_interface["jp_policy_in"] = None
    pim_interface["jp_policy_out"] = None
    pim_interface["jp_type_in"] = None
    pim_interface["jp_type_out"] = None
    pim_interface["jp_bidir"] = False
    pim_interface["isauth"] = False

    if body:
        all_lines = body.splitlines()

        for each in all_lines:
            if "jp-policy" in each:
                policy_name = re.search(
                    r"ip pim jp-policy(?: prefix-list)? (\S+)(?: \S+)?", each
                ).group(1)
                if "prefix-list" in each:
                    ptype = "prefix"
                else:
                    ptype = "routemap"
                if "out" in each:
                    pim_interface["jp_policy_out"] = policy_name
                    pim_interface["jp_type_out"] = ptype
                elif "in" in each:
                    pim_interface["jp_policy_in"] = policy_name
                    pim_interface["jp_type_in"] = ptype
                else:
                    pim_interface["jp_policy_in"] = policy_name
                    pim_interface["jp_policy_out"] = policy_name
                    pim_interface["jp_bidir"] = True
            elif "neighbor-policy" in each:
                pim_interface["neighbor_policy"] = re.search(
                    r"ip pim neighbor-policy(?: prefix-list)? (\S+)", each
                ).group(1)
                if "prefix-list" in each:
                    pim_interface["neighbor_type"] = "prefix"
                else:
                    pim_interface["neighbor_type"] = "routemap"
            elif "ah-md5" in each:
                pim_interface["isauth"] = True
            elif "sparse-mode" in each:
                pim_interface["sparse"] = True
            elif "bfd-instance" in each:
                m = re.search(
                    r"ip pim bfd-instance(?P<disable> disable)?", each
                )
                if m:
                    pim_interface["bfd"] = (
                        "disable" if m.group("disable") else "enable"
                    )
            elif "border" in each:
                pim_interface["border"] = True
            elif "hello-interval" in each:
                pim_interface["hello_interval"] = re.search(
                    r"ip pim hello-interval (\d+)", body
                ).group(1)
            elif "dr-priority" in each:
                pim_interface["dr_prio"] = re.search(
                    r"ip pim dr-priority (\d+)", body
                ).group(1)

    return pim_interface


def fix_delta(delta, existing):
    for key in list(delta):
        if key in ["dr_prio", "hello_interval", "sparse", "border"]:
            if (
                delta.get(key) == PARAM_TO_DEFAULT_KEYMAP.get(key)
                and existing.get(key) is None
            ):
                delta.pop(key)
    return delta


def config_pim_interface(delta, existing, jp_bidir, isauth):
    command = None
    commands = []

    delta = fix_delta(delta, existing)

    if jp_bidir:
        if delta.get("jp_policy_in") or delta.get("jp_policy_out"):
            if existing.get("jp_type_in") == "prefix":
                command = "no ip pim jp-policy prefix-list {0}".format(
                    existing.get("jp_policy_in")
                )
            else:
                command = "no ip pim jp-policy {0}".format(
                    existing.get("jp_policy_in")
                )
            if command:
                commands.append(command)

    for k, v in delta.items():
        if k in [
            "bfd",
            "dr_prio",
            "hello_interval",
            "hello_auth_key",
            "border",
            "sparse",
        ]:
            if k == "bfd":
                command = BFD_KEYMAP[v]
            elif v:
                command = PARAM_TO_COMMAND_KEYMAP.get(k).format(v)
            elif k == "hello_auth_key":
                if isauth:
                    command = "no ip pim hello-authentication ah-md5"
            else:
                command = "no " + PARAM_TO_COMMAND_KEYMAP.get(k).format(v)

            if command:
                commands.append(command)
        elif k in [
            "neighbor_policy",
            "jp_policy_in",
            "jp_policy_out",
            "neighbor_type",
        ]:
            if k in ["neighbor_policy", "neighbor_type"]:
                temp = delta.get("neighbor_policy") or existing.get(
                    "neighbor_policy"
                )
                if delta.get("neighbor_type") == "prefix":
                    command = PARAM_TO_COMMAND_KEYMAP.get(k).format(temp)
                elif delta.get("neighbor_type") == "routemap":
                    command = "ip pim neighbor-policy {0}".format(temp)
                elif existing.get("neighbor_type") == "prefix":
                    command = PARAM_TO_COMMAND_KEYMAP.get(k).format(temp)
                elif existing.get("neighbor_type") == "routemap":
                    command = "ip pim neighbor-policy {0}".format(temp)
            elif k in ["jp_policy_in", "jp_type_in"]:
                temp = delta.get("jp_policy_in") or existing.get(
                    "jp_policy_in"
                )
                if delta.get("jp_type_in") == "prefix":
                    command = PARAM_TO_COMMAND_KEYMAP.get(k).format(temp)
                elif delta.get("jp_type_in") == "routemap":
                    command = "ip pim jp-policy {0} in".format(temp)
                elif existing.get("jp_type_in") == "prefix":
                    command = PARAM_TO_COMMAND_KEYMAP.get(k).format(temp)
                elif existing.get("jp_type_in") == "routemap":
                    command = "ip pim jp-policy {0} in".format(temp)
            elif k in ["jp_policy_out", "jp_type_out"]:
                temp = delta.get("jp_policy_out") or existing.get(
                    "jp_policy_out"
                )
                if delta.get("jp_type_out") == "prefix":
                    command = PARAM_TO_COMMAND_KEYMAP.get(k).format(temp)
                elif delta.get("jp_type_out") == "routemap":
                    command = "ip pim jp-policy {0} out".format(temp)
                elif existing.get("jp_type_out") == "prefix":
                    command = PARAM_TO_COMMAND_KEYMAP.get(k).format(temp)
                elif existing.get("jp_type_out") == "routemap":
                    command = "ip pim jp-policy {0} out".format(temp)
            if command:
                commands.append(command)
        command = None

    if "no ip pim sparse-mode" in commands:
        # sparse is long-running on some platforms, process it last
        commands.remove("no ip pim sparse-mode")
        commands.append("no ip pim sparse-mode")
    return commands


def get_pim_interface_defaults():

    args = dict(
        dr_prio=PARAM_TO_DEFAULT_KEYMAP.get("dr_prio"),
        bfd=PARAM_TO_DEFAULT_KEYMAP.get("bfd"),
        border=PARAM_TO_DEFAULT_KEYMAP.get("border"),
        sparse=PARAM_TO_DEFAULT_KEYMAP.get("sparse"),
        hello_interval=PARAM_TO_DEFAULT_KEYMAP.get("hello_interval"),
        hello_auth_key=PARAM_TO_DEFAULT_KEYMAP.get("hello_auth_key"),
    )

    default = dict(
        (param, value) for (param, value) in args.items() if value is not None
    )

    return default


def default_pim_interface_policies(existing, jp_bidir):
    commands = []

    if jp_bidir:
        if existing.get("jp_policy_in") or existing.get("jp_policy_out"):
            if existing.get("jp_type_in") == "prefix":
                command = "no ip pim jp-policy prefix-list {0}".format(
                    existing.get("jp_policy_in")
                )
        if command:
            commands.append(command)

    elif not jp_bidir:
        command = None
        for k in existing:
            if k == "jp_policy_in":
                if existing.get("jp_policy_in"):
                    if existing.get("jp_type_in") == "prefix":
                        command = "no ip pim jp-policy prefix-list {0} in".format(
                            existing.get("jp_policy_in")
                        )
                    else:
                        command = "no ip pim jp-policy {0} in".format(
                            existing.get("jp_policy_in")
                        )
            elif k == "jp_policy_out":
                if existing.get("jp_policy_out"):
                    if existing.get("jp_type_out") == "prefix":
                        command = "no ip pim jp-policy prefix-list {0} out".format(
                            existing.get("jp_policy_out")
                        )
                    else:
                        command = "no ip pim jp-policy {0} out".format(
                            existing.get("jp_policy_out")
                        )
            if command:
                commands.append(command)
            command = None

    if existing.get("neighbor_policy"):
        command = "no ip pim neighbor-policy"
        commands.append(command)

    return commands


def config_pim_interface_defaults(existing, jp_bidir, isauth):
    command = []

    # returns a dict
    defaults = get_pim_interface_defaults()
    delta = dict(set(defaults.items()).difference(existing.items()))
    if delta:
        # returns a list
        command = config_pim_interface(delta, existing, jp_bidir, isauth)
    comm = default_pim_interface_policies(existing, jp_bidir)
    if comm:
        for each in comm:
            command.append(each)

    return command


def normalize_proposed_values(proposed):
    keys = proposed.keys()
    if "bfd" in keys:
        # bfd is a tri-state string: enable, disable, default
        proposed["bfd"] = proposed["bfd"].lower()
    if "hello_interval" in keys:
        proposed["hello_interval"] = str(proposed["hello_interval"] * 1000)


def main():
    argument_spec = dict(
        interface=dict(type="str", required=True),
        sparse=dict(type="bool", default=False),
        dr_prio=dict(type="str"),
        hello_auth_key=dict(type="str"),
        hello_interval=dict(type="int"),
        jp_policy_out=dict(type="str"),
        jp_policy_in=dict(type="str"),
        jp_type_out=dict(type="str", choices=["prefix", "routemap"]),
        jp_type_in=dict(type="str", choices=["prefix", "routemap"]),
        bfd=dict(type="str", choices=["enable", "disable", "default"]),
        border=dict(type="bool", default=False),
        neighbor_policy=dict(type="str"),
        neighbor_type=dict(type="str", choices=["prefix", "routemap"]),
        state=dict(
            type="str",
            default="present",
            choices=["absent", "default", "present"],
        ),
    )
    argument_spec.update(nxos_argument_spec)

    module = AnsibleModule(
        argument_spec=argument_spec, supports_check_mode=True
    )

    warnings = list()
    results = {"changed": False, "commands": [], "warnings": warnings}

    state = module.params["state"]
    interface = module.params["interface"]
    jp_type_in = module.params["jp_type_in"]
    jp_type_out = module.params["jp_type_out"]
    jp_policy_in = module.params["jp_policy_in"]
    jp_policy_out = module.params["jp_policy_out"]
    neighbor_policy = module.params["neighbor_policy"]
    neighbor_type = module.params["neighbor_type"]

    intf_type = get_interface_type(interface)
    if get_interface_mode(interface, intf_type, module) == "layer2":
        module.fail_json(msg="this module only works on Layer 3 interfaces.")

    if jp_policy_in:
        if not jp_type_in:
            module.fail_json(
                msg="jp_type_in required when using jp_policy_in."
            )
    if jp_policy_out:
        if not jp_type_out:
            module.fail_json(
                msg="jp_type_out required when using jp_policy_out."
            )
    if neighbor_policy:
        if not neighbor_type:
            module.fail_json(
                msg="neighbor_type required when using neighbor_policy."
            )

    get_existing = get_pim_interface(module, interface)
    existing, jp_bidir, isauth = local_existing(get_existing)

    args = PARAM_TO_COMMAND_KEYMAP.keys()
    proposed = dict(
        (k, v) for k, v in module.params.items() if v is not None and k in args
    )
    normalize_proposed_values(proposed)

    delta = dict(set(proposed.items()).difference(existing.items()))

    commands = []
    if state == "present":
        if delta:
            command = config_pim_interface(delta, existing, jp_bidir, isauth)
            if command:
                commands.append(command)
    elif state == "default" or state == "absent":
        defaults = config_pim_interface_defaults(existing, jp_bidir, isauth)
        if defaults:
            commands.append(defaults)

    if commands:
        commands.insert(0, ["interface {0}".format(interface)])

    cmds = flatten_list(commands)
    if cmds:
        results["changed"] = True
        if not module.check_mode:
            load_config(module, cmds)
        if "configure" in cmds:
            cmds.pop(0)

    results["commands"] = cmds

    module.exit_json(**results)


if __name__ == "__main__":
    main()
