# Copyright (c) 2016 Cisco Systems Inc.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import ast
import functools

from neutron_lib.api import converters as conv
from neutron_lib.api.definitions import address_scope as as_def
from neutron_lib.api.definitions import network as net_def
from neutron_lib.api.definitions import port as port_def
from neutron_lib.api.definitions import subnet as subnet_def
from neutron_lib.api import extensions
from neutron_lib.api import validators as valid
from oslo_log import log as logging
import six

from gbpservice._i18n import _

ALIAS = 'cisco-apic'

DIST_NAMES = 'apic:distinguished_names'
SYNC_STATE = 'apic:synchronization_state'
NAT_TYPE = 'apic:nat_type'
SNAT_HOST_POOL = 'apic:snat_host_pool'
ACTIVE_ACTIVE_AAP = 'apic:active_active_aap'
EXTERNAL_CIDRS = 'apic:external_cidrs'
SVI = 'apic:svi'
BGP = 'apic:bgp_enable'
BGP_ASN = 'apic:bgp_asn'
BGP_TYPE = 'apic:bgp_type'
NESTED_DOMAIN_NAME = 'apic:nested_domain_name'
NESTED_DOMAIN_TYPE = 'apic:nested_domain_type'
NESTED_DOMAIN_INFRA_VLAN = 'apic:nested_domain_infra_vlan'
NESTED_DOMAIN_ALLOWED_VLANS = 'apic:nested_domain_allowed_vlans'
NESTED_DOMAIN_SERVICE_VLAN = 'apic:nested_domain_service_vlan'
NESTED_DOMAIN_NODE_NETWORK_VLAN = 'apic:nested_domain_node_network_vlan'
EXTRA_PROVIDED_CONTRACTS = 'apic:extra_provided_contracts'
EXTRA_CONSUMED_CONTRACTS = 'apic:extra_consumed_contracts'
EPG_CONTRACT_MASTERS = 'apic:epg_contract_masters'
ERSPAN_CONFIG = 'apic:erspan_config'
POLICY_ENFORCEMENT_PREF = 'apic:policy_enforcement_pref'
SNAT_SUBNET_ONLY = 'apic:snat_subnet_only'
EPG_SUBNET = 'apic:epg_subnet'
NO_NAT_CIDRS = 'apic:no_nat_cidrs'
MULTI_EXT_NETS = 'apic:multi_ext_nets'
ADVERTISED_EXTERNALLY = 'apic:advertised_externally'
SHARED_BETWEEN_VRFS = 'apic:shared_between_vrfs'
ROUTER_GW_IP_POOL = 'apic:router_gw_ip_pool'

BD = 'BridgeDomain'
EPG = 'EndpointGroup'
SUBNET = 'Subnet'
VRF = 'VRF'
EXTERNAL_NETWORK = 'ExternalNetwork'
AP = 'ApplicationProfile'

SYNC_SYNCED = 'synced'
SYNC_BUILD = 'build'
SYNC_ERROR = 'error'
SYNC_NOT_APPLICABLE = 'N/A'

VLANS_LIST = 'vlans_list'
VLAN_RANGES = 'vlan_ranges'
APIC_MAX_VLAN = 4093
APIC_MIN_VLAN = 1
VLAN_RANGE_START = 'start'
VLAN_RANGE_END = 'end'

ERSPAN_DEST_IP = 'dest_ip'
ERSPAN_FLOW_ID = 'flow_id'
ERSPAN_DIRECTION = 'direction'

LOG = logging.getLogger(__name__)


def _validate_apic_vlan(data, key_specs=None):
    if data is None:
        return
    try:
        val = int(data)
        if val >= APIC_MIN_VLAN and val <= APIC_MAX_VLAN:
            return
        msg = ("Invalid value for VLAN: '%s'") % data
        LOG.debug(msg)
        return msg
    except (ValueError, TypeError):
        msg = ("Invalid data format for VLAN: '%s'") % data
        LOG.debug(msg)
        return msg


def _validate_apic_vlan_range(data, key_specs=None):
    if data is None:
        return

    expected_keys = [VLAN_RANGE_START, VLAN_RANGE_END]
    msg = valid._verify_dict_keys(expected_keys, data)
    if msg:
        return msg
    for k in expected_keys:
        msg = _validate_apic_vlan(data[k])
        if msg:
            return msg
    if int(data[VLAN_RANGE_START]) > int(data[VLAN_RANGE_END]):
        msg = ("Invalid start, end for VLAN range %s") % data
        return msg


def _validate_erspan_flow_id(data, key_specs=None):
    if data is None:
        return
    msg = valid.validate_non_negative(data)
    if int(data) > 1023:
        msg = ("ERSPAN flow ID must be less than 1023 (was %s)") % data
    elif int(data) == 0:
        msg = ("ERSPAN flow ID must be greater than 0 (was %s)") % data
    return msg


def _validate_erspan_configs(data, valid_values=None):
    """Validate a list of unique ERSPAN configurations.

    :param data: The data to validate. To be valid it must be a list like
        structure of ERSPAN config dicts, each containing 'dest_ip' and
        'flow_id' key values.
    :param valid_values: Not used!
    :returns: None if data is a valid list of unique ERSPAN config dicts,
        otherwise a human readable message indicating why validation failed.
    """
    if not isinstance(data, list):
        msg = ("Invalid data format for ERSPAN config: '%s'") % data
        LOG.debug(msg)
        return msg

    expected_keys = (ERSPAN_DEST_IP, ERSPAN_FLOW_ID,)
    erspan_configs = []
    for erspan_config in data:
        msg = valid._verify_dict_keys(expected_keys, erspan_config, False)
        if msg:
            return msg
        msg = _validate_erspan_flow_id(erspan_config[ERSPAN_FLOW_ID])
        if msg:
            return msg
        msg = valid.validate_ip_address(erspan_config[ERSPAN_DEST_IP])
        if msg:
            return msg
        if erspan_config in erspan_configs:
            msg = ("Duplicate ERSPAN config '%s'") % erspan_config
            LOG.debug(msg)
            return msg
        erspan_configs.append(erspan_config)


def _validate_dict_or_string(data, key_specs=None):
    if data is None:
        return

    if isinstance(data, str) or isinstance(data, six.string_types):
        try:
            data = ast.literal_eval(data)
        except Exception:
            msg = _("Extension %s cannot be converted to dict") % data
            return msg

    return valid.validate_dict_or_none(data, key_specs)


def convert_apic_vlan(value):
    if value is None:
        return
    else:
        return int(value)


def convert_apic_none_to_empty_list(value):
    if value is None:
        return []
    if isinstance(value, str) or isinstance(value, six.string_types):
        value = ast.literal_eval(value)
    return value


def convert_nested_domain_allowed_vlans(value):
    if value is None:
        return

    if isinstance(value, str) or isinstance(value, six.string_types):
        value = ast.literal_eval(value)

    vlans_list = []
    if VLANS_LIST in value:
        for vlan in value[VLANS_LIST]:
            vlans_list.append(convert_apic_vlan(vlan))
    if VLAN_RANGES in value:
        for vlan_range in value[VLAN_RANGES]:
            for vrng in [VLAN_RANGE_START, VLAN_RANGE_END]:
                vlan_range[vrng] = convert_apic_vlan(vlan_range[vrng])
            vlans_list.extend(list(range(vlan_range[VLAN_RANGE_START],
                vlan_range[VLAN_RANGE_END] + 1)))
    # eliminate duplicates
    vlans_list = list(set(vlans_list))
    # sort
    vlans_list.sort()
    value[VLANS_LIST] = vlans_list
    return value


valid.validators['type:apic_vlan'] = _validate_apic_vlan
valid.validators['type:apic_vlan_list'] = functools.partial(
        valid._validate_list_of_items, _validate_apic_vlan)
valid.validators['type:apic_vlan_range_list'] = functools.partial(
        valid._validate_list_of_items, _validate_apic_vlan_range)
valid.validators['type:dict_or_string'] = _validate_dict_or_string
valid.validators['type:apic_erspan_flow_id'] = _validate_erspan_flow_id
valid.validators['type:apic_erspan_configs'] = _validate_erspan_configs


APIC_ATTRIBUTES = {
    DIST_NAMES: {'allow_post': False, 'allow_put': False, 'is_visible': True},
    SYNC_STATE: {'allow_post': False, 'allow_put': False, 'is_visible': True}
}

ERSPAN_KEY_SPECS = [
    {ERSPAN_DEST_IP: {'type:ip_address': None,
                      'required': True},
     ERSPAN_FLOW_ID: {'type:apic_erspan_flow_id': None,
                      'required': True},
     ERSPAN_DIRECTION: {'type:values': ['in', 'out', 'both'],
                        'default': 'both'}},
]

EPG_CONTRACT_MASTER_KEY_SPECS = [
    # key spec for opt_name in _VALID_BLANK_EXTRA_DHCP_OPTS
    {'app_profile_name': {'type:not_empty_string': None,
                          'required': True},
     'name': {'type:not_empty_string': None,
              'required': True}},
]

PORT_ATTRIBUTES = {
    ERSPAN_CONFIG: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': None,
        'convert_to': convert_apic_none_to_empty_list,
        'validate': {'type:apic_erspan_configs': None},
    },
}

NET_ATTRIBUTES = {
    SVI: {
        'allow_post': True, 'allow_put': False,
        'is_visible': True, 'default': False,
        'convert_to': conv.convert_to_boolean,
    },
    BGP: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': False,
        'convert_to': conv.convert_to_boolean,
    },
    BGP_TYPE: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': 'default_export',
        'validate': {'type:values': ['default_export', '']},
    },
    BGP_ASN: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': "0",
        'validate': {'type:non_negative': None},
    },
    NESTED_DOMAIN_NAME: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': '',
        'validate': {'type:string': None},
    },
    NESTED_DOMAIN_TYPE: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': '',
        'validate': {'type:string': None},
    },
    NESTED_DOMAIN_INFRA_VLAN: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': None,
        'validate': {'type:apic_vlan': None},
        'convert_to': convert_apic_vlan,
    },
    NESTED_DOMAIN_SERVICE_VLAN: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': None,
        'validate': {'type:apic_vlan': None},
        'convert_to': convert_apic_vlan,
    },
    NESTED_DOMAIN_NODE_NETWORK_VLAN: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': None,
        'validate': {'type:apic_vlan': None},
        'convert_to': convert_apic_vlan,
    },
    NESTED_DOMAIN_ALLOWED_VLANS: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': None,
        'validate': {
            'type:dict_or_string': {
                VLANS_LIST: {'type:apic_vlan_list': None},
                VLAN_RANGES: {'type:apic_vlan_range_list': None},
            }
        },
        'convert_to': convert_nested_domain_allowed_vlans,
    },
    EXTRA_PROVIDED_CONTRACTS: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': None,
        'convert_to': convert_apic_none_to_empty_list,
        'validate': {'type:list_of_unique_strings': None},
    },
    EXTRA_CONSUMED_CONTRACTS: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': None,
        'convert_to': convert_apic_none_to_empty_list,
        'validate': {'type:list_of_unique_strings': None},
    },
    EPG_CONTRACT_MASTERS: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': None,
        'convert_to': convert_apic_none_to_empty_list,
        'validate': {'type:list_of_any_key_specs_or_none':
                     EPG_CONTRACT_MASTER_KEY_SPECS},
    },
    DIST_NAMES: {
        # DN of corresponding APIC L3Out external network or BD.
        # It can be specified only on create.
        # Change 'allow_put' if updates on other DNs is allowed later,
        # and validate that ExternalNetwork DN may not be updated.
        'allow_post': True, 'allow_put': False,
        'is_visible': True,
        'default': None,
        'validate': {
            'type:dict_or_none': {
                EXTERNAL_NETWORK: {'type:string': None,
                                   'required': False},
                BD: {'type:string': None,
                     'required': False}
            },
        }
    },
    POLICY_ENFORCEMENT_PREF: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': 'unenforced',
        'validate': {'type:values': ['unenforced', 'enforced', '']},
    },
    NO_NAT_CIDRS: {
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': None,
        'convert_to': convert_apic_none_to_empty_list,
        'validate': {'type:list_of_unique_strings': None},
    },
    MULTI_EXT_NETS: {
        'allow_post': True, 'allow_put': False,
        'is_visible': True, 'default': False,
        'convert_to': conv.convert_to_boolean,
    },
}

EXT_NET_ATTRIBUTES = {
    NAT_TYPE: {
        # whether NAT is enabled, and if so its type
        'allow_post': True, 'allow_put': False,
        'is_visible': True, 'default': 'distributed',
        'validate': {'type:values': ['distributed', 'edge', '']},
    },
    EXTERNAL_CIDRS: {
        # Restrict external traffic to specified addresses
        'allow_put': True, 'allow_post': True,
        'is_visible': True, 'default': ['0.0.0.0/0'],
        'convert_to': convert_apic_none_to_empty_list,
        'validate': {'type:subnet_list': None},
    },
}

EXT_SUBNET_ATTRIBUTES = {
    SNAT_HOST_POOL: {
        # Whether an external subnet should be used as a pool
        # for allocating host-based SNAT addresses.
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': False,
        'convert_to': conv.convert_to_boolean,
    },
    ACTIVE_ACTIVE_AAP: {
        # Whether a subnet will support the active active AAP or not.
        'allow_post': True, 'allow_put': False,
        'is_visible': True, 'default': False,
        'convert_to': conv.convert_to_boolean,
    },
    SNAT_SUBNET_ONLY: {
        # Whether this subnet can be used for assigning snat addresses only
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': False,
        'convert_to': conv.convert_to_boolean,
    },
    EPG_SUBNET: {
        # Whether this subnet is EPG subnet or regular subnet
        'allow_post': True, 'allow_put': False,
        'is_visible': True, 'default': False,
        'convert_to': conv.convert_to_boolean,
    },
    ADVERTISED_EXTERNALLY: {
        # Whether this subnet is visible outside of ACI or not
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': True,
        'convert_to': conv.convert_to_boolean,
    },
    SHARED_BETWEEN_VRFS: {
        # Whether this subnet is seen across VRFs or only its own
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': False,
        'convert_to': conv.convert_to_boolean,
    },
    ROUTER_GW_IP_POOL: {
        # Whether this subnet is used for snat ip allocation
        'allow_post': True, 'allow_put': True,
        'is_visible': True, 'default': False,
        'convert_to': conv.convert_to_boolean,
    }
}

ADDRESS_SCOPE_ATTRIBUTES = {
    DIST_NAMES: {
        # DN of corresponding APIC VRF; can be specified only on create.
        # Change 'allow_put' if updates on other DNs is allowed later,
        # and validate that VRF DN may not be updated.
        'allow_post': True, 'allow_put': False,
        'is_visible': True,
        'default': None,
        'validate': {
            'type:dict_or_none': {
                VRF: {'type:string': None,
                      'required': True}
            }
        }
    }
}


EXTENDED_ATTRIBUTES_2_0 = {
    port_def.COLLECTION_NAME: dict(
        list(APIC_ATTRIBUTES.items()) + list(PORT_ATTRIBUTES.items())),
    net_def.COLLECTION_NAME: dict(
        list(APIC_ATTRIBUTES.items()) + list(EXT_NET_ATTRIBUTES.items()) +
        list(NET_ATTRIBUTES.items())),
    subnet_def.COLLECTION_NAME: dict(
        list(APIC_ATTRIBUTES.items()) + list(EXT_SUBNET_ATTRIBUTES.items())),
    as_def.COLLECTION_NAME: dict(
        list(APIC_ATTRIBUTES.items()) + list(ADDRESS_SCOPE_ATTRIBUTES.items()))
}


class Cisco_apic(extensions.ExtensionDescriptor):

    @classmethod
    def get_name(cls):
        return "Cisco APIC"

    @classmethod
    def get_alias(cls):
        return ALIAS

    @classmethod
    def get_description(cls):
        return ("Extension exposing mapping of Neutron resources to Cisco "
                "APIC constructs")

    @classmethod
    def get_updated(cls):
        return "2016-03-31T12:00:00-00:00"

    def get_extended_resources(self, version):
        if version == "2.0":
            return EXTENDED_ATTRIBUTES_2_0
        else:
            return {}
