# Copyright (C) 2014 Nippon Telegraph and Telephone Corporation.
#
# 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.

"""
 Prefix related APIs.
"""
import logging

from os_ken.lib.packet.bgp import EvpnEsi
from os_ken.lib.packet.bgp import EvpnNLRI
from os_ken.lib.packet.bgp import EvpnEthernetAutoDiscoveryNLRI
from os_ken.lib.packet.bgp import EvpnMacIPAdvertisementNLRI
from os_ken.lib.packet.bgp import EvpnInclusiveMulticastEthernetTagNLRI
from os_ken.lib.packet.bgp import EvpnEthernetSegmentNLRI
from os_ken.lib.packet.bgp import EvpnIpPrefixNLRI
from os_ken.lib.packet.bgp import BGPPathAttributePmsiTunnel
from os_ken.lib.packet.bgp import FlowSpecIPv4NLRI
from os_ken.lib.packet.bgp import FlowSpecIPv6NLRI
from os_ken.lib.packet.bgp import FlowSpecVPNv4NLRI
from os_ken.lib.packet.bgp import FlowSpecVPNv6NLRI
from os_ken.lib.packet.bgp import FlowSpecL2VPNNLRI
from os_ken.lib.packet.bgp import BGPFlowSpecTrafficRateCommunity
from os_ken.lib.packet.bgp import BGPFlowSpecTrafficActionCommunity
from os_ken.lib.packet.bgp import BGPFlowSpecRedirectCommunity
from os_ken.lib.packet.bgp import BGPFlowSpecTrafficMarkingCommunity
from os_ken.lib.packet.bgp import BGPFlowSpecVlanActionCommunity
from os_ken.lib.packet.bgp import BGPFlowSpecTPIDActionCommunity

from os_ken.services.protocols.bgp.api.base import EVPN_ROUTE_TYPE
from os_ken.services.protocols.bgp.api.base import EVPN_ESI
from os_ken.services.protocols.bgp.api.base import EVPN_ETHERNET_TAG_ID
from os_ken.services.protocols.bgp.api.base import REDUNDANCY_MODE
from os_ken.services.protocols.bgp.api.base import MAC_ADDR
from os_ken.services.protocols.bgp.api.base import IP_ADDR
from os_ken.services.protocols.bgp.api.base import IP_PREFIX
from os_ken.services.protocols.bgp.api.base import GW_IP_ADDR
from os_ken.services.protocols.bgp.api.base import MPLS_LABELS
from os_ken.services.protocols.bgp.api.base import NEXT_HOP
from os_ken.services.protocols.bgp.api.base import PREFIX
from os_ken.services.protocols.bgp.api.base import RegisterWithArgChecks
from os_ken.services.protocols.bgp.api.base import ROUTE_DISTINGUISHER
from os_ken.services.protocols.bgp.api.base import VPN_LABEL
from os_ken.services.protocols.bgp.api.base import EVPN_VNI
from os_ken.services.protocols.bgp.api.base import TUNNEL_TYPE
from os_ken.services.protocols.bgp.api.base import PMSI_TUNNEL_TYPE
from os_ken.services.protocols.bgp.api.base import MAC_MOBILITY
from os_ken.services.protocols.bgp.api.base import TUNNEL_ENDPOINT_IP
from os_ken.services.protocols.bgp.api.base import FLOWSPEC_FAMILY
from os_ken.services.protocols.bgp.api.base import FLOWSPEC_RULES
from os_ken.services.protocols.bgp.api.base import FLOWSPEC_ACTIONS
from os_ken.services.protocols.bgp.base import add_bgp_error_metadata
from os_ken.services.protocols.bgp.base import PREFIX_ERROR_CODE
from os_ken.services.protocols.bgp.base import validate
from os_ken.services.protocols.bgp.core import BgpCoreError
from os_ken.services.protocols.bgp.core_manager import CORE_MANAGER
from os_ken.services.protocols.bgp.rtconf.base import ConfigValueError
from os_ken.services.protocols.bgp.rtconf.base import RuntimeConfigError
from os_ken.services.protocols.bgp.rtconf.vrfs import VRF_RF
from os_ken.services.protocols.bgp.rtconf.vrfs import VRF_RF_IPV4
from os_ken.services.protocols.bgp.rtconf.vrfs import VRF_RF_L2_EVPN
from os_ken.services.protocols.bgp.utils import validation

LOG = logging.getLogger('bgpspeaker.api.prefix')

# Maximum value of the Ethernet Tag ID
EVPN_MAX_ET = EvpnNLRI.MAX_ET

# ESI Types
ESI_TYPE_ARBITRARY = EvpnEsi.ARBITRARY
ESI_TYPE_LACP = EvpnEsi.LACP
ESI_TYPE_L2_BRIDGE = EvpnEsi.L2_BRIDGE
ESI_TYPE_MAC_BASED = EvpnEsi.MAC_BASED
ESI_TYPE_ROUTER_ID = EvpnEsi.ROUTER_ID
ESI_TYPE_AS_BASED = EvpnEsi.AS_BASED
SUPPORTED_ESI_TYPES = [
    ESI_TYPE_ARBITRARY,
    ESI_TYPE_LACP,
    ESI_TYPE_L2_BRIDGE,
    ESI_TYPE_MAC_BASED,
    ESI_TYPE_ROUTER_ID,
    ESI_TYPE_AS_BASED,
]

# Constants used in API calls for EVPN
EVPN_ETH_AUTO_DISCOVERY = EvpnEthernetAutoDiscoveryNLRI.ROUTE_TYPE_NAME
EVPN_MAC_IP_ADV_ROUTE = EvpnMacIPAdvertisementNLRI.ROUTE_TYPE_NAME
EVPN_MULTICAST_ETAG_ROUTE = (
    EvpnInclusiveMulticastEthernetTagNLRI.ROUTE_TYPE_NAME)
EVPN_ETH_SEGMENT = EvpnEthernetSegmentNLRI.ROUTE_TYPE_NAME
EVPN_IP_PREFIX_ROUTE = EvpnIpPrefixNLRI.ROUTE_TYPE_NAME
SUPPORTED_EVPN_ROUTE_TYPES = [
    EVPN_ETH_AUTO_DISCOVERY,
    EVPN_MAC_IP_ADV_ROUTE,
    EVPN_MULTICAST_ETAG_ROUTE,
    EVPN_ETH_SEGMENT,
    EVPN_IP_PREFIX_ROUTE,
]

# Constants used in API calls for Flow Specification
FLOWSPEC_FAMILY_IPV4 = FlowSpecIPv4NLRI.FLOWSPEC_FAMILY
FLOWSPEC_FAMILY_IPV6 = FlowSpecIPv6NLRI.FLOWSPEC_FAMILY
FLOWSPEC_FAMILY_VPNV4 = FlowSpecVPNv4NLRI.FLOWSPEC_FAMILY
FLOWSPEC_FAMILY_VPNV6 = FlowSpecVPNv6NLRI.FLOWSPEC_FAMILY
FLOWSPEC_FAMILY_L2VPN = FlowSpecL2VPNNLRI.FLOWSPEC_FAMILY
SUPPORTED_FLOWSPEC_FAMILIES = (
    FLOWSPEC_FAMILY_IPV4,
    FLOWSPEC_FAMILY_IPV6,
    FLOWSPEC_FAMILY_VPNV4,
    FLOWSPEC_FAMILY_VPNV6,
    FLOWSPEC_FAMILY_L2VPN,
)

# Constants for the Traffic Filtering Actions of Flow Specification
# Constants for the Traffic Filtering Actions of Flow Specification.
FLOWSPEC_ACTION_TRAFFIC_RATE = BGPFlowSpecTrafficRateCommunity.ACTION_NAME
FLOWSPEC_ACTION_TRAFFIC_ACTION = BGPFlowSpecTrafficActionCommunity.ACTION_NAME
FLOWSPEC_ACTION_REDIRECT = BGPFlowSpecRedirectCommunity.ACTION_NAME
FLOWSPEC_ACTION_TRAFFIC_MARKING = BGPFlowSpecTrafficMarkingCommunity.ACTION_NAME
FLOWSPEC_ACTION_VLAN = BGPFlowSpecVlanActionCommunity.ACTION_NAME
FLOWSPEC_ACTION_TPID = BGPFlowSpecTPIDActionCommunity.ACTION_NAME

SUPPORTTED_FLOWSPEC_ACTIONS = (
    FLOWSPEC_ACTION_TRAFFIC_RATE,
    FLOWSPEC_ACTION_TRAFFIC_ACTION,
    FLOWSPEC_ACTION_REDIRECT,
    FLOWSPEC_ACTION_TRAFFIC_MARKING,
    FLOWSPEC_ACTION_VLAN,
    FLOWSPEC_ACTION_TPID,
)


# Constants for ESI Label extended community
REDUNDANCY_MODE_ALL_ACTIVE = 'all_active'
REDUNDANCY_MODE_SINGLE_ACTIVE = 'single_active'
SUPPORTED_REDUNDANCY_MODES = [
    REDUNDANCY_MODE_ALL_ACTIVE,
    REDUNDANCY_MODE_SINGLE_ACTIVE,
]

# Constants for BGP Tunnel Encapsulation Attribute
TUNNEL_TYPE_VXLAN = 'vxlan'
TUNNEL_TYPE_NVGRE = 'nvgre'
TUNNEL_TYPE_MPLS = 'mpls'
TUNNEL_TYPE_MPLS_IN_GRE = 'mpls_in_gre'
TUNNEL_TYPE_VXLAN_GRE = 'vxlan_gre'
SUPPORTED_TUNNEL_TYPES = [
    TUNNEL_TYPE_VXLAN,
    TUNNEL_TYPE_NVGRE,
    TUNNEL_TYPE_MPLS,
    TUNNEL_TYPE_MPLS_IN_GRE,
    TUNNEL_TYPE_VXLAN_GRE,
]
# Constants for PMSI Tunnel Attribute
PMSI_TYPE_NO_TUNNEL_INFO = (
    BGPPathAttributePmsiTunnel.TYPE_NO_TUNNEL_INFORMATION_PRESENT
)
PMSI_TYPE_INGRESS_REP = (
    BGPPathAttributePmsiTunnel.TYPE_INGRESS_REPLICATION
)
SUPPORTED_PMSI_TUNNEL_TYPES = [
    PMSI_TYPE_NO_TUNNEL_INFO,
    PMSI_TYPE_INGRESS_REP,
]


@add_bgp_error_metadata(code=PREFIX_ERROR_CODE,
                        sub_code=1,
                        def_desc='Unknown error related to operation on '
                        'prefixes')
class PrefixError(RuntimeConfigError):
    pass


@validate(name=PREFIX)
def is_valid_prefix(prefix):
    if not (validation.is_valid_ipv4_prefix(prefix)
            or validation.is_valid_ipv6_prefix(prefix)):
        raise ConfigValueError(conf_name=PREFIX,
                               conf_value=prefix)


@validate(name=NEXT_HOP)
def is_valid_next_hop(next_hop):
    if not (validation.is_valid_ipv4(next_hop)
            or validation.is_valid_ipv6(next_hop)):
        raise ConfigValueError(conf_name=NEXT_HOP,
                               conf_value=next_hop)


@validate(name=EVPN_ROUTE_TYPE)
def is_valid_evpn_route_type(route_type):
    if route_type not in SUPPORTED_EVPN_ROUTE_TYPES:
        raise ConfigValueError(conf_name=EVPN_ROUTE_TYPE,
                               conf_value=route_type)


@validate(name=EVPN_ESI)
def is_valid_esi(esi):
    if not validation.is_valid_esi(esi):
        raise ConfigValueError(conf_name=EVPN_ESI,
                               conf_value=esi)


@validate(name=EVPN_ETHERNET_TAG_ID)
def is_valid_ethernet_tag_id(ethernet_tag_id):
    if not validation.is_valid_ethernet_tag_id(ethernet_tag_id):
        raise ConfigValueError(conf_name=EVPN_ETHERNET_TAG_ID,
                               conf_value=ethernet_tag_id)


@validate(name=REDUNDANCY_MODE)
def is_valid_redundancy_mode(redundancy_mode):
    if redundancy_mode not in SUPPORTED_REDUNDANCY_MODES:
        raise ConfigValueError(conf_name=REDUNDANCY_MODE,
                               conf_value=redundancy_mode)


@validate(name=MAC_ADDR)
def is_valid_mac_addr(addr):
    if not validation.is_valid_mac(addr):
        raise ConfigValueError(conf_name=MAC_ADDR,
                               conf_value=addr)


@validate(name=IP_ADDR)
def is_valid_ip_addr(addr):
    # Note: Allows empty IP Address (means length=0).
    # e.g.) L2VPN MAC advertisement of Cisco NX-OS
    if not (addr is None
            or validation.is_valid_ipv4(addr)
            or validation.is_valid_ipv6(addr)):
        raise ConfigValueError(conf_name=IP_ADDR,
                               conf_value=addr)


@validate(name=IP_PREFIX)
def is_valid_ip_prefix(prefix):
    if not (validation.is_valid_ipv4_prefix(prefix)
            or validation.is_valid_ipv6_prefix(prefix)):
        raise ConfigValueError(conf_name=IP_PREFIX,
                               conf_value=prefix)


@validate(name=GW_IP_ADDR)
def is_valid_gw_ip_addr(addr):
    if not (validation.is_valid_ipv4(addr)
            or validation.is_valid_ipv6(addr)):
        raise ConfigValueError(conf_name=GW_IP_ADDR,
                               conf_value=addr)


@validate(name=MPLS_LABELS)
def is_valid_mpls_labels(labels):
    if not validation.is_valid_mpls_labels(labels):
        raise ConfigValueError(conf_name=MPLS_LABELS,
                               conf_value=labels)


@validate(name=EVPN_VNI)
def is_valid_vni(vni):
    if not validation.is_valid_vni(vni):
        raise ConfigValueError(conf_name=EVPN_VNI,
                               conf_value=vni)


@validate(name=TUNNEL_TYPE)
def is_valid_tunnel_type(tunnel_type):
    if tunnel_type not in SUPPORTED_TUNNEL_TYPES:
        raise ConfigValueError(conf_name=TUNNEL_TYPE,
                               conf_value=tunnel_type)


@validate(name=PMSI_TUNNEL_TYPE)
def is_valid_pmsi_tunnel_type(pmsi_tunnel_type):
    if pmsi_tunnel_type not in SUPPORTED_PMSI_TUNNEL_TYPES:
        raise ConfigValueError(conf_name=PMSI_TUNNEL_TYPE,
                               conf_value=pmsi_tunnel_type)


@validate(name=FLOWSPEC_FAMILY)
def is_valid_flowspec_family(flowspec_family):
    if flowspec_family not in SUPPORTED_FLOWSPEC_FAMILIES:
        raise ConfigValueError(conf_name=FLOWSPEC_FAMILY,
                               conf_value=flowspec_family)


@validate(name=FLOWSPEC_RULES)
def is_valid_flowspec_rules(rules):
    if not isinstance(rules, dict):
        raise ConfigValueError(conf_name=FLOWSPEC_RULES,
                               conf_value=rules)


@validate(name=FLOWSPEC_ACTIONS)
def is_valid_flowspec_actions(actions):
    for k in actions:
        if k not in SUPPORTTED_FLOWSPEC_ACTIONS:
            raise ConfigValueError(conf_name=FLOWSPEC_ACTIONS,
                                   conf_value=actions)


@RegisterWithArgChecks(name='prefix.add_local',
                       req_args=[ROUTE_DISTINGUISHER, PREFIX, NEXT_HOP],
                       opt_args=[VRF_RF])
def add_local(route_dist, prefix, next_hop, route_family=VRF_RF_IPV4):
    """Adds *prefix* from VRF identified by *route_dist* and sets the source as
    network controller.
    """
    try:
        # Create new path and insert into appropriate VRF table.
        tm = CORE_MANAGER.get_core_service().table_manager
        label = tm.update_vrf_table(route_dist, prefix, next_hop, route_family)
        # Currently we only allocate one label per local_prefix,
        # so we share first label from the list.
        if label:
            label = label[0]

        # Send success response with new label.
        return [{ROUTE_DISTINGUISHER: route_dist, PREFIX: prefix,
                 VRF_RF: route_family, VPN_LABEL: label}]
    except BgpCoreError as e:
        raise PrefixError(desc=e)


@RegisterWithArgChecks(name='prefix.delete_local',
                       req_args=[ROUTE_DISTINGUISHER, PREFIX],
                       opt_args=[VRF_RF])
def delete_local(route_dist, prefix, route_family=VRF_RF_IPV4):
    """Deletes/withdraws *prefix* from VRF identified by *route_dist* and
    source as network controller.
    """
    try:
        tm = CORE_MANAGER.get_core_service().table_manager
        tm.update_vrf_table(route_dist, prefix,
                            route_family=route_family, is_withdraw=True)
        # Send success response.
        return [{ROUTE_DISTINGUISHER: route_dist, PREFIX: prefix,
                 VRF_RF: route_family}]
    except BgpCoreError as e:
        raise PrefixError(desc=e)


# =============================================================================
# BGP EVPN Routes related APIs
# =============================================================================

@RegisterWithArgChecks(name='evpn_prefix.add_local',
                       req_args=[EVPN_ROUTE_TYPE, ROUTE_DISTINGUISHER,
                                 NEXT_HOP],
                       opt_args=[EVPN_ESI, EVPN_ETHERNET_TAG_ID,
                                 REDUNDANCY_MODE, MAC_ADDR, IP_ADDR, IP_PREFIX,
                                 GW_IP_ADDR, EVPN_VNI, TUNNEL_TYPE,
                                 PMSI_TUNNEL_TYPE, TUNNEL_ENDPOINT_IP,
                                 MAC_MOBILITY])
def add_evpn_local(route_type, route_dist, next_hop, **kwargs):
    """Adds EVPN route from VRF identified by *route_dist*.
    """

    if (route_type in [EVPN_ETH_AUTO_DISCOVERY, EVPN_ETH_SEGMENT]
            and kwargs['esi'] == 0):
        raise ConfigValueError(conf_name=EVPN_ESI,
                               conf_value=kwargs['esi'])

    try:
        # Create new path and insert into appropriate VRF table.
        tm = CORE_MANAGER.get_core_service().table_manager
        label = tm.update_vrf_table(route_dist, next_hop=next_hop,
                                    route_family=VRF_RF_L2_EVPN,
                                    route_type=route_type, **kwargs)
        # Currently we only allocate one label per local route,
        # so we share first label from the list.
        if label:
            label = label[0]

        # Send success response with new label.
        return [{EVPN_ROUTE_TYPE: route_type,
                 ROUTE_DISTINGUISHER: route_dist,
                 VRF_RF: VRF_RF_L2_EVPN,
                 VPN_LABEL: label}.update(kwargs)]
    except BgpCoreError as e:
        raise PrefixError(desc=e)


@RegisterWithArgChecks(name='evpn_prefix.delete_local',
                       req_args=[EVPN_ROUTE_TYPE, ROUTE_DISTINGUISHER],
                       opt_args=[EVPN_ESI, EVPN_ETHERNET_TAG_ID, MAC_ADDR,
                                 IP_ADDR, IP_PREFIX, EVPN_VNI])
def delete_evpn_local(route_type, route_dist, **kwargs):
    """Deletes/withdraws EVPN route from VRF identified by *route_dist*.
    """
    try:
        tm = CORE_MANAGER.get_core_service().table_manager
        tm.update_vrf_table(route_dist,
                            route_family=VRF_RF_L2_EVPN,
                            route_type=route_type, is_withdraw=True, **kwargs)
        # Send success response.
        return [{EVPN_ROUTE_TYPE: route_type,
                 ROUTE_DISTINGUISHER: route_dist,
                 VRF_RF: VRF_RF_L2_EVPN}.update(kwargs)]
    except BgpCoreError as e:
        raise PrefixError(desc=e)


# =============================================================================
# BGP Flow Specification Routes related APIs
# =============================================================================

@RegisterWithArgChecks(
    name='flowspec.add_local',
    req_args=[FLOWSPEC_FAMILY, ROUTE_DISTINGUISHER, FLOWSPEC_RULES],
    opt_args=[FLOWSPEC_ACTIONS])
def add_flowspec_local(flowspec_family, route_dist, rules, **kwargs):
    """Adds Flow Specification route from VRF identified by *route_dist*.
    """
    try:
        # Create new path and insert into appropriate VRF table.
        tm = CORE_MANAGER.get_core_service().table_manager
        tm.update_flowspec_vrf_table(
            flowspec_family=flowspec_family, route_dist=route_dist,
            rules=rules, **kwargs)

        # Send success response.
        return [{FLOWSPEC_FAMILY: flowspec_family,
                 ROUTE_DISTINGUISHER: route_dist,
                 FLOWSPEC_RULES: rules}.update(kwargs)]

    except BgpCoreError as e:
        raise PrefixError(desc=e)


@RegisterWithArgChecks(
    name='flowspec.del_local',
    req_args=[FLOWSPEC_FAMILY, ROUTE_DISTINGUISHER, FLOWSPEC_RULES])
def del_flowspec_local(flowspec_family, route_dist, rules):
    """Deletes/withdraws Flow Specification route from VRF identified
    by *route_dist*.
    """
    try:
        tm = CORE_MANAGER.get_core_service().table_manager
        tm.update_flowspec_vrf_table(
            flowspec_family=flowspec_family, route_dist=route_dist,
            rules=rules, is_withdraw=True)

        # Send success response.
        return [{FLOWSPEC_FAMILY: flowspec_family,
                 ROUTE_DISTINGUISHER: route_dist,
                 FLOWSPEC_RULES: rules}]

    except BgpCoreError as e:
        raise PrefixError(desc=e)
