#!/usr/bin/python
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
#

ANSIBLE_METADATA = {'metadata_version': '1.1',
                    'status': ['preview'],
                    'supported_by': 'network'}


DOCUMENTATION = '''
---
module: nxos_bgp_neighbor_af
extends_documentation_fragment: nxos
version_added: "2.2"
short_description: Manages BGP address-family's neighbors configuration.
description:
  - Manages BGP address-family's neighbors configurations on NX-OS switches.
author: Gabriele Gerbino (@GGabriele)
notes:
  - Tested against NXOSv 7.3.(0)D1(1) on VIRL
  - C(state=absent) removes the whole BGP address-family's
    neighbor configuration.
  - Default, when supported, removes properties
  - In order to default maximum-prefix configuration, only
    C(max_prefix_limit=default) is needed.
options:
  asn:
    description:
      - BGP autonomous system number. Valid values are String,
        Integer in ASPLAIN or ASDOT notation.
    required: true
  vrf:
    description:
      - Name of the VRF. The name 'default' is a valid VRF representing
        the global bgp.
    default: default
  neighbor:
    description:
      - Neighbor Identifier. Valid values are string. Neighbors may use
        IPv4 or IPv6 notation, with or without prefix length.
    required: true
  afi:
    description:
      - Address Family Identifier.
    required: true
    choices: ['ipv4','ipv6', 'vpnv4', 'vpnv6', 'l2vpn']
  safi:
    description:
      - Sub Address Family Identifier.
    required: true
    choices: ['unicast','multicast', 'evpn']
  additional_paths_receive:
    description:
      - Valid values are enable for basic command enablement; disable
        for disabling the command at the neighbor af level
        (it adds the disable keyword to the basic command); and inherit
        to remove the command at this level (the command value is
        inherited from a higher BGP layer).
    choices: ['enable','disable', 'inherit']
  additional_paths_send:
    description:
      - Valid values are enable for basic command enablement; disable
        for disabling the command at the neighbor af level
        (it adds the disable keyword to the basic command); and inherit
        to remove the command at this level (the command value is
        inherited from a higher BGP layer).
    choices: ['enable','disable', 'inherit']
  advertise_map_exist:
    description:
      - Conditional route advertisement. This property requires two
        route maps, an advertise-map and an exist-map. Valid values are
        an array specifying both the advertise-map name and the exist-map
        name, or simply 'default' e.g. ['my_advertise_map',
        'my_exist_map']. This command is mutually exclusive with the
        advertise_map_non_exist property.
  advertise_map_non_exist:
    description:
      - Conditional route advertisement. This property requires two
        route maps, an advertise-map and an exist-map. Valid values are
        an array specifying both the advertise-map name and the
        non-exist-map name, or simply 'default' e.g.
        ['my_advertise_map', 'my_non_exist_map']. This command is mutually
        exclusive with the advertise_map_exist property.
  allowas_in:
    description:
      - Activate allowas-in property
    type: bool
  allowas_in_max:
    description:
      - Max-occurrences value for allowas_in. Valid values are
        an integer value or 'default'. This is mutually exclusive with
        allowas_in.
  as_override:
    description:
      - Activate the as-override feature.
    type: bool
  default_originate:
    description:
      - Activate the default-originate feature.
    type: bool
  default_originate_route_map:
    description:
      - Route-map for the default_originate property.
        Valid values are a string defining a route-map name,
        or 'default'. This is mutually exclusive with
        default_originate.
  disable_peer_as_check:
    description:
      - Disable checking of peer AS-number while advertising
    type: bool
    version_added: 2.5
  filter_list_in:
    description:
      - Valid values are a string defining a filter-list name,
        or 'default'.
  filter_list_out:
    description:
      - Valid values are a string defining a filter-list name,
        or 'default'.
  max_prefix_limit:
    description:
      - maximum-prefix limit value. Valid values are an integer value
        or 'default'.
  max_prefix_interval:
    description:
      - Optional restart interval. Valid values are an integer.
        Requires max_prefix_limit. May not be combined with max_prefix_warning.
  max_prefix_threshold:
    description:
      - Optional threshold percentage at which to generate a warning.
        Valid values are an integer value.
        Requires max_prefix_limit.
  max_prefix_warning:
    description:
      - Optional warning-only keyword. Requires max_prefix_limit. May not be
        combined with max_prefix_interval.
    type: bool
  next_hop_self:
    description:
      - Activate the next-hop-self feature.
    type: bool
  next_hop_third_party:
    description:
      - Activate the next-hop-third-party feature.
    type: bool
  prefix_list_in:
    description:
      - Valid values are a string defining a prefix-list name,
        or 'default'.
  prefix_list_out:
    description:
      - Valid values are a string defining a prefix-list name,
        or 'default'.
  route_map_in:
    description:
      - Valid values are a string defining a route-map name,
        or 'default'.
  route_map_out:
    description:
      - Valid values are a string defining a route-map name,
        or 'default'.
  route_reflector_client:
    description:
      - Router reflector client.
    type: bool
  send_community:
    description:
      - send-community attribute.
    choices: ['none', 'both', 'extended', 'standard', 'default']
  soft_reconfiguration_in:
    description:
      - Valid values are 'enable' for basic command enablement; 'always'
        to add the always keyword to the basic command; and 'inherit' to
        remove the command at this level (the command value is inherited
        from a higher BGP layer).
    choices: ['enable','always','inherit']
  soo:
    description:
      - Site-of-origin. Valid values are a string defining a VPN
        extcommunity or 'default'.
  suppress_inactive:
    description:
      - suppress-inactive feature.
    type: bool
  unsuppress_map:
    description:
      - unsuppress-map. Valid values are a string defining a route-map
        name or 'default'.
  weight:
    description:
      - Weight value. Valid values are an integer value or 'default'.
  state:
    description:
      - Determines whether the config should be present or not
        on the device.
    default: present
    choices: ['present','absent']
'''
EXAMPLES = '''
- name: configure RR client
  nxos_bgp_neighbor_af:
    asn: 65535
    neighbor: '192.0.2.3'
    afi: ipv4
    safi: unicast
    route_reflector_client: true
    state: present
'''

RETURN = '''
commands:
  description: commands sent to the device
  returned: always
  type: list
  sample: ["router bgp 65535", "neighbor 192.0.2.3",
           "address-family ipv4 unicast", "route-reflector-client"]
'''

import re

from ansible.module_utils.network.nxos.nxos import get_config, load_config
from ansible.module_utils.network.nxos.nxos import nxos_argument_spec, check_args
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.network.common.config import CustomNetworkConfig


BOOL_PARAMS = [
    'allowas_in',
    'as_override',
    'default_originate',
    'disable_peer_as_check',
    'next_hop_self',
    'next_hop_third_party',
    'route_reflector_client',
    'suppress_inactive'
]
PARAM_TO_COMMAND_KEYMAP = {
    'afi': 'address-family',
    'asn': 'router bgp',
    'neighbor': 'neighbor',
    'additional_paths_receive': 'capability additional-paths receive',
    'additional_paths_send': 'capability additional-paths send',
    'advertise_map_exist': 'advertise-map exist-map',
    'advertise_map_non_exist': 'advertise-map non-exist-map',
    'allowas_in': 'allowas-in',
    'allowas_in_max': 'allowas-in',
    'as_override': 'as-override',
    'default_originate': 'default-originate',
    'default_originate_route_map': 'default-originate route-map',
    'disable_peer_as_check': 'disable-peer-as-check',
    'filter_list_in': 'filter-list in',
    'filter_list_out': 'filter-list out',
    'max_prefix_limit': 'maximum-prefix',
    'max_prefix_interval': 'maximum-prefix interval',
    'max_prefix_threshold': 'maximum-prefix threshold',
    'max_prefix_warning': 'maximum-prefix warning',
    'next_hop_self': 'next-hop-self',
    'next_hop_third_party': 'next-hop-third-party',
    'prefix_list_in': 'prefix-list in',
    'prefix_list_out': 'prefix-list out',
    'route_map_in': 'route-map in',
    'route_map_out': 'route-map out',
    'route_reflector_client': 'route-reflector-client',
    'safi': 'address-family',
    'send_community': 'send-community',
    'soft_reconfiguration_in': 'soft-reconfiguration inbound',
    'soo': 'soo',
    'suppress_inactive': 'suppress-inactive',
    'unsuppress_map': 'unsuppress-map',
    'weight': 'weight',
    'vrf': 'vrf'
}


def get_value(arg, config, module):
    custom = [
        'additional_paths_send',
        'additional_paths_receive',
        'max_prefix_limit',
        'max_prefix_interval',
        'max_prefix_threshold',
        'max_prefix_warning',
        'send_community',
        'soft_reconfiguration_in'
    ]
    command = PARAM_TO_COMMAND_KEYMAP[arg]
    has_command = re.search(r'^\s+{0}\s*'.format(command), config, re.M)
    has_command_val = re.search(r'(?:{0}\s)(?P<value>.*)$'.format(command), config, re.M)
    value = ''

    if arg in custom:
        value = get_custom_value(arg, config, module)

    elif arg == 'next_hop_third_party':
        has_no_command = re.search(r'^\s+no\s+{0}\s*$'.format(command), config, re.M)
        value = False
        if not has_no_command:
            value = True

    elif arg in BOOL_PARAMS:
        value = False
        if has_command:
            value = True

    elif command.startswith('advertise-map'):
        value = []
        has_adv_map = re.search(r'{0}\s(?P<value1>.*)\s{1}\s(?P<value2>.*)$'.format(*command.split()), config, re.M)
        if has_adv_map:
            value = list(has_adv_map.groups())

    elif command.split()[0] in ['filter-list', 'prefix-list', 'route-map']:
        has_cmd_direction_val = re.search(r'{0}\s(?P<value>.*)\s{1}$'.format(*command.split()), config, re.M)
        if has_cmd_direction_val:
            value = has_cmd_direction_val.group('value')

    elif has_command_val:
        value = has_command_val.group('value')

    return value


def get_custom_value(arg, config, module):
    command = PARAM_TO_COMMAND_KEYMAP.get(arg)
    splitted_config = config.splitlines()
    value = ''

    command_re = re.compile(r'\s+{0}\s*'.format(command), re.M)
    has_command = command_re.search(config)
    command_val_re = re.compile(r'(?:{0}\s)(?P<value>.*)$'.format(command), re.M)
    has_command_val = command_val_re.search(config)

    if arg.startswith('additional_paths'):
        value = 'inherit'
        for line in splitted_config:
            if command in line:
                if 'disable' in line:
                    value = 'disable'
                else:
                    value = 'enable'
    elif arg.startswith('max_prefix'):
        for line in splitted_config:
            if 'maximum-prefix' in line:
                splitted_line = line.split()
                if arg == 'max_prefix_limit':
                    value = splitted_line[1]
                elif arg == 'max_prefix_interval' and 'restart' in line:
                    value = splitted_line[-1]
                elif arg == 'max_prefix_threshold' and len(splitted_line) > 2:
                    try:
                        int(splitted_line[2])
                        value = splitted_line[2]
                    except ValueError:
                        value = ''
                elif arg == 'max_prefix_warning':
                    value = 'warning-only' in line
    elif arg == 'soft_reconfiguration_in':
        value = 'inherit'
        for line in splitted_config:
            if command in line:
                if 'always' in line:
                    value = 'always'
                else:
                    value = 'enable'

    elif arg == 'send_community':
        value = 'none'
        for line in splitted_config:
            if command in line:
                if 'extended' in line:
                    if value == 'standard':
                        value = 'both'
                    else:
                        value = 'extended'
                elif 'both' in line:
                    value = 'both'
                else:
                    value = 'standard'

    return value


def get_existing(module, args, warnings):
    existing = {}
    netcfg = CustomNetworkConfig(indent=2, contents=get_config(module))

    asn_regex = re.compile(r'.*router\sbgp\s(?P<existing_asn>\d+(\.\d+)?).*', re.S)
    match_asn = asn_regex.match(str(netcfg))

    if match_asn:
        existing_asn = match_asn.group('existing_asn')
        parents = ["router bgp {0}".format(existing_asn)]

        if module.params['vrf'] != 'default':
            parents.append('vrf {0}'.format(module.params['vrf']))

        parents.append('neighbor {0}'.format(module.params['neighbor']))
        parents.append('address-family {0} {1}'.format(module.params['afi'], module.params['safi']))
        config = netcfg.get_section(parents)

        if config:
            for arg in args:
                if arg not in ['asn', 'vrf', 'neighbor', 'afi', 'safi']:
                    existing[arg] = get_value(arg, config, module)

            existing['asn'] = existing_asn
            existing['neighbor'] = module.params['neighbor']
            existing['vrf'] = module.params['vrf']
            existing['afi'] = module.params['afi']
            existing['safi'] = module.params['safi']
    else:
        warnings.append("The BGP process didn't exist but the task just created it.")

    return existing


def apply_key_map(key_map, table):
    new_dict = {}
    for key in table:
        new_key = key_map.get(key)
        if new_key:
            new_dict[new_key] = table.get(key)

    return new_dict


def get_default_command(key, value, existing_commands):
    command = ''
    if existing_commands.get(key):
        existing_value = existing_commands.get(key)
        if value == 'inherit':
            if existing_value != 'inherit':
                command = 'no {0}'.format(key)
        else:
            if key == 'advertise-map exist-map':
                command = 'no advertise-map {0} exist-map {1}'.format(
                    existing_value[0], existing_value[1])
            elif key == 'advertise-map non-exist-map':
                command = 'no advertise-map {0} non-exist-map {1}'.format(
                    existing_value[0], existing_value[1])
            elif key == 'filter-list in':
                command = 'no filter-list {0} in'.format(existing_value)
            elif key == 'filter-list out':
                command = 'no filter-list {0} out'.format(existing_value)
            elif key == 'prefix-list in':
                command = 'no prefix-list {0} in'.format(existing_value)
            elif key == 'prefix-list out':
                command = 'no prefix-list {0} out'.format(existing_value)
            elif key == 'route-map in':
                command = 'no route-map {0} in'.format(existing_value)
            elif key == 'route-map out':
                command = 'no route-map {0} out'.format(existing_value)
            elif key.startswith('maximum-prefix'):
                command = 'no maximum-prefix'
            elif key == 'allowas-in max':
                command = ['no allowas-in {0}'.format(existing_value)]
                command.append('allowas-in')
            else:
                command = 'no {0} {1}'.format(key, existing_value)
    else:
        if key.replace(' ', '_').replace('-', '_') in BOOL_PARAMS:
            command = 'no {0}'.format(key)
    return command


def fix_proposed(module, existing, proposed):
    allowas_in = proposed.get('allowas_in')
    allowas_in_max = proposed.get('allowas_in_max')

    if allowas_in_max and not allowas_in:
        proposed.pop('allowas_in_max')
    elif allowas_in and allowas_in_max:
        proposed.pop('allowas_in')

    if existing.get('send_community') == 'none' and proposed.get('send_community') == 'default':
        proposed.pop('send_community')
    return proposed


def state_present(module, existing, proposed, candidate):
    commands = list()
    proposed = fix_proposed(module, existing, proposed)

    proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed)
    existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing)
    for key, value in proposed_commands.items():
        if value in ['inherit', 'default']:
            command = get_default_command(key, value, existing_commands)

            if isinstance(command, str):
                if command and command not in commands:
                    commands.append(command)
            elif isinstance(command, list):
                for cmd in command:
                    if cmd not in commands:
                        commands.append(cmd)

        elif key.startswith('maximum-prefix'):
            if module.params['max_prefix_limit'] != 'default':
                command = 'maximum-prefix {0}'.format(module.params['max_prefix_limit'])
                if module.params['max_prefix_threshold']:
                    command += ' {0}'.format(module.params['max_prefix_threshold'])
                if module.params['max_prefix_interval']:
                    command += ' restart {0}'.format(module.params['max_prefix_interval'])
                elif module.params['max_prefix_warning']:
                    command += ' warning-only'
                commands.append(command)

        elif value is True:
            commands.append(key)
        elif value is False:
            commands.append('no {0}'.format(key))
        elif key == 'address-family':
            commands.append("address-family {0} {1}".format(module.params['afi'], module.params['safi']))
        elif key.startswith('capability additional-paths'):
            command = key
            if value == 'disable':
                command += ' disable'
            commands.append(command)
        elif key.startswith('advertise-map'):
            direction = key.split()[1]
            commands.append('advertise-map {1} {0} {2}'.format(direction, *value))
        elif key.split()[0] in ['filter-list', 'prefix-list', 'route-map']:
            commands.append('{1} {0} {2}'.format(value, *key.split()))

        elif key == 'soft-reconfiguration inbound':
            command = ''
            if value == 'enable':
                command = key
            elif value == 'always':
                command = '{0} {1}'.format(key, value)
            commands.append(command)
        elif key == 'send-community':
            command = key
            if value in ['standard', 'extended']:
                commands.append('no ' + key + ' both')
            command += ' {0}'.format(value)
            commands.append(command)
        else:
            command = '{0} {1}'.format(key, value)
            commands.append(command)

    if commands:
        parents = ['router bgp {0}'.format(module.params['asn'])]
        if module.params['vrf'] != 'default':
            parents.append('vrf {0}'.format(module.params['vrf']))

        parents.append('neighbor {0}'.format(module.params['neighbor']))

        af_command = 'address-family {0} {1}'.format(
            module.params['afi'], module.params['safi'])
        parents.append(af_command)
        if af_command in commands:
            commands.remove(af_command)
        candidate.add(commands, parents=parents)


def state_absent(module, existing, candidate):
    commands = []
    parents = ["router bgp {0}".format(module.params['asn'])]
    if module.params['vrf'] != 'default':
        parents.append('vrf {0}'.format(module.params['vrf']))

    parents.append('neighbor {0}'.format(module.params['neighbor']))
    commands.append('no address-family {0} {1}'.format(
        module.params['afi'], module.params['safi']))
    candidate.add(commands, parents=parents)


def main():
    argument_spec = dict(
        asn=dict(required=True, type='str'),
        vrf=dict(required=False, type='str', default='default'),
        neighbor=dict(required=True, type='str'),
        afi=dict(required=True, type='str'),
        safi=dict(required=True, type='str'),
        additional_paths_receive=dict(required=False, type='str', choices=['enable', 'disable', 'inherit']),
        additional_paths_send=dict(required=False, type='str', choices=['enable', 'disable', 'inherit']),
        advertise_map_exist=dict(required=False, type='list'),
        advertise_map_non_exist=dict(required=False, type='list'),
        allowas_in=dict(required=False, type='bool'),
        allowas_in_max=dict(required=False, type='str'),
        as_override=dict(required=False, type='bool'),
        default_originate=dict(required=False, type='bool'),
        default_originate_route_map=dict(required=False, type='str'),
        disable_peer_as_check=dict(required=False, type='bool'),
        filter_list_in=dict(required=False, type='str'),
        filter_list_out=dict(required=False, type='str'),
        max_prefix_limit=dict(required=False, type='str'),
        max_prefix_interval=dict(required=False, type='str'),
        max_prefix_threshold=dict(required=False, type='str'),
        max_prefix_warning=dict(required=False, type='bool'),
        next_hop_self=dict(required=False, type='bool'),
        next_hop_third_party=dict(required=False, type='bool'),
        prefix_list_in=dict(required=False, type='str'),
        prefix_list_out=dict(required=False, type='str'),
        route_map_in=dict(required=False, type='str'),
        route_map_out=dict(required=False, type='str'),
        route_reflector_client=dict(required=False, type='bool'),
        send_community=dict(required=False, choices=['none', 'both', 'extended', 'standard', 'default']),
        soft_reconfiguration_in=dict(required=False, type='str', choices=['enable', 'always', 'inherit']),
        soo=dict(required=False, type='str'),
        suppress_inactive=dict(required=False, type='bool'),
        unsuppress_map=dict(required=False, type='str'),
        weight=dict(required=False, type='str'),
        state=dict(choices=['present', 'absent'], default='present', required=False),
    )
    argument_spec.update(nxos_argument_spec)

    module = AnsibleModule(
        argument_spec=argument_spec,
        mutually_exclusive=[['advertise_map_exist', 'advertise_map_non_exist'],
                            ['max_prefix_interval', 'max_prefix_warning'],
                            ['default_originate', 'default_originate_route_map'],
                            ['allowas_in', 'allowas_in_max']],
        supports_check_mode=True,
    )

    warnings = list()
    check_args(module, warnings)
    result = dict(changed=False, warnings=warnings)

    state = module.params['state']
    for key in ['max_prefix_interval', 'max_prefix_warning', 'max_prefix_threshold']:
        if module.params[key] and not module.params['max_prefix_limit']:
            module.fail_json(
                msg='max_prefix_limit is required when using %s' % key
            )
    if module.params['vrf'] == 'default' and module.params['soo']:
        module.fail_json(msg='SOO is only allowed in non-default VRF')

    args = PARAM_TO_COMMAND_KEYMAP.keys()
    existing = get_existing(module, args, warnings)

    if existing.get('asn') and state == 'present':
        if existing.get('asn') != module.params['asn']:
            module.fail_json(msg='Another BGP ASN already exists.',
                             proposed_asn=module.params['asn'],
                             existing_asn=existing.get('asn'))

    for param in ['advertise_map_exist', 'advertise_map_non_exist']:
        if module.params[param] == ['default']:
            module.params[param] = 'default'

    proposed_args = dict((k, v) for k, v in module.params.items() if v is not None and k in args)

    proposed = {}
    for key, value in proposed_args.items():
        if key not in ['asn', 'vrf', 'neighbor']:
            if not isinstance(value, list):
                if str(value).lower() == 'true':
                    value = True
                elif str(value).lower() == 'false':
                    value = False
                elif str(value).lower() == 'default':
                    if key in BOOL_PARAMS:
                        value = False
                    else:
                        value = 'default'
                elif key == 'send_community' and str(value).lower() == 'none':
                    value = 'default'
            if existing.get(key) != value:
                proposed[key] = value

    candidate = CustomNetworkConfig(indent=3)
    if state == 'present':
        state_present(module, existing, proposed, candidate)
    elif state == 'absent' and existing:
        state_absent(module, existing, candidate)

    if candidate:
        candidate = candidate.items_text()
        load_config(module, candidate)
        result['changed'] = True
        result['commands'] = candidate
    else:
        result['commands'] = []

    module.exit_json(**result)


if __name__ == '__main__':
    main()
