# 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.

"""
 Runtime configuration that applies to all bgp sessions, i.e. global settings.
"""
import logging
import numbers

from ryu.lib import ip

from ryu.services.protocols.bgp.utils.validation import is_valid_ipv4
from ryu.services.protocols.bgp.utils.validation import is_valid_asn

from ryu.services.protocols.bgp import rtconf
from ryu.services.protocols.bgp.rtconf.base import BaseConf
from ryu.services.protocols.bgp.rtconf.base import BaseConfListener
from ryu.services.protocols.bgp.rtconf.base import compute_optional_conf
from ryu.services.protocols.bgp.rtconf.base import ConfigTypeError
from ryu.services.protocols.bgp.rtconf.base import ConfigValueError
from ryu.services.protocols.bgp.rtconf.base import MissingRequiredConf
from ryu.services.protocols.bgp.rtconf.base import validate

LOG = logging.getLogger('bgpspeaker.rtconf.common')


# Global configuration settings.
LOCAL_AS = 'local_as'
ROUTER_ID = 'router_id'
CLUSTER_ID = 'cluster_id'
LABEL_RANGE = 'label_range'
LABEL_RANGE_MAX = 'max'
LABEL_RANGE_MIN = 'min'
LOCAL_PREF = 'local_pref'

# Similar to Cisco command 'allowas-in'. Allows the local ASN in the path.
# Facilitates auto rd, auto rt import/export
# ("rd auto/route-target both auto") and simplified spine/leaf architectures,
# sharing an ASN between e.g. leafs.
ALLOW_LOCAL_AS_IN_COUNT = 'allow_local_as_in_count'

# Configuration that can be set at global level as well as per context
# (session/vrf) level
# Nested configuration override global or higher level configuration as they
# are more granular.
# TODO(apgw-dev) Nested configuration overriding higher level configuration is
# currently low priority

# Similar to Cisco command 'bgp refresh stalepath-time'. To cause the router to
# remove stale routes from the BGP table even if the router does not receive a
# Route-Refresh EOR message The bgp refresh stalepath-time command is not
# needed under normal circumstances.
# TODO(PH): Support this feature (currently low priority)
REFRESH_STALEPATH_TIME = 'refresh_stalepath_time'

# Similar to Cisco command 'bgp refresh max-eor-time'. The bgp refresh max-eor-
# time command is not needed under normal  circumstances. You might configure
# the bgp refresh max-eor-time command in the event of continuous route
# flapping, when the router is unable to generate a Route- Refresh EOR message,
# in which case a Route-Refresh EOR is generated after the timer expires.
# TODO(PH): Support this feature (currently low priority)
REFRESH_MAX_EOR_TIME = 'refresh_max_eor_time'

BGP_CONN_RETRY_TIME = 'bgp_conn_retry_time'
BGP_SERVER_HOSTS = 'bgp_server_hosts'
BGP_SERVER_PORT = 'bgp_server_port'
TCP_CONN_TIMEOUT = 'tcp_conn_timeout'
MAX_PATH_EXT_RTFILTER_ALL = 'maximum_paths_external_rtfilter_all'


# Valid default values of some settings.
DEFAULT_LABEL_RANGE = (100, 100000)
DEFAULT_REFRESH_STALEPATH_TIME = 0
DEFAULT_REFRESH_MAX_EOR_TIME = 0
DEFAULT_BGP_SERVER_HOSTS = ('0.0.0.0', '::')
DEFAULT_BGP_SERVER_PORT = 179
DEFAULT_TCP_CONN_TIMEOUT = 30
DEFAULT_BGP_CONN_RETRY_TIME = 30
DEFAULT_MED = 0
DEFAULT_MAX_PATH_EXT_RTFILTER_ALL = True
DEFAULT_LOCAL_PREF = 100


@validate(name=ALLOW_LOCAL_AS_IN_COUNT)
def validate_allow_local_as_in_count(count):
    if not isinstance(count, numbers.Integral):
        raise ConfigTypeError(desc=('Configuration value for %s has to be '
                                    'integral type' % ALLOW_LOCAL_AS_IN_COUNT))
    if count < 0:
        raise ConfigValueError(desc='Invalid local AS count %s' % count)

    return count


@validate(name=LOCAL_AS)
def validate_local_as(asn):
    if asn is None:
        raise MissingRequiredConf(conf_name=LOCAL_AS)

    if not is_valid_asn(asn):
        raise ConfigValueError(desc='Invalid local_as configuration value: %s'
                               % asn)
    return asn


@validate(name=ROUTER_ID)
def validate_router_id(router_id):
    if not router_id:
        raise MissingRequiredConf(conf_name=ROUTER_ID)

    if not isinstance(router_id, str):
        raise ConfigTypeError(conf_name=ROUTER_ID)
    if not is_valid_ipv4(router_id):
        raise ConfigValueError(desc='Invalid router id %s' % router_id)

    return router_id


@validate(name=CLUSTER_ID)
def validate_router_id(cluster_id):
    if not isinstance(cluster_id, str):
        raise ConfigTypeError(conf_name=CLUSTER_ID)
    if not is_valid_ipv4(cluster_id):
        raise ConfigValueError(desc='Invalid cluster id %s' % cluster_id)

    return cluster_id


@validate(name=REFRESH_STALEPATH_TIME)
def validate_refresh_stalepath_time(rst):
    if not isinstance(rst, numbers.Integral):
        raise ConfigTypeError(desc=('Configuration value for %s has to be '
                                    'integral type' % REFRESH_STALEPATH_TIME))
    if rst < 0:
        raise ConfigValueError(desc='Invalid refresh stalepath time %s' % rst)

    return rst


@validate(name=REFRESH_MAX_EOR_TIME)
def validate_refresh_max_eor_time(rmet):
    if not isinstance(rmet, numbers.Integral):
        raise ConfigTypeError(desc=('Configuration value for %s has to be of '
                                    'integral type ' % REFRESH_MAX_EOR_TIME))
    if rmet < 0:
        raise ConfigValueError(desc='Invalid refresh stalepath time %s' % rmet)

    return rmet


@validate(name=LABEL_RANGE)
def validate_label_range(label_range):
    min_label, max_label = label_range
    if (not min_label or
            not max_label or
            not isinstance(min_label, numbers.Integral) or
            not isinstance(max_label, numbers.Integral) or min_label < 17 or
            min_label >= max_label):
        raise ConfigValueError(desc=('Invalid label_range configuration value:'
                                     ' (%s).' % label_range))

    return label_range


@validate(name=BGP_SERVER_HOSTS)
def validate_bgp_server_hosts(hosts):
    for host in hosts:
        if not ip.valid_ipv4(host) and not ip.valid_ipv6(host):
            raise ConfigTypeError(desc=('Invalid bgp sever hosts '
                                        'configuration value %s' % hosts))

    return hosts


@validate(name=BGP_SERVER_PORT)
def validate_bgp_server_port(server_port):
    if not isinstance(server_port, numbers.Integral):
        raise ConfigTypeError(desc=('Invalid bgp sever port configuration '
                                    'value %s' % server_port))
    if server_port < 0 or server_port > 65535:
        raise ConfigValueError(desc='Invalid server port %s' % server_port)

    return server_port


@validate(name=TCP_CONN_TIMEOUT)
def validate_tcp_conn_timeout(tcp_conn_timeout):
    # TODO(apgw-dev) made-up some valid values for this settings, check if we
    # have a standard value in any routers
    if not isinstance(tcp_conn_timeout, numbers.Integral):
        raise ConfigTypeError(desc=('Invalid tcp connection timeout '
                                    'configuration value %s' %
                                    tcp_conn_timeout))

    if tcp_conn_timeout < 10:
        raise ConfigValueError(desc=('Invalid tcp connection timeout'
                                     ' configuration value %s' %
                                     tcp_conn_timeout))

    return tcp_conn_timeout


@validate(name=BGP_CONN_RETRY_TIME)
def validate_bgp_conn_retry_time(bgp_conn_retry_time):
    if not isinstance(bgp_conn_retry_time, numbers.Integral):
        raise ConfigTypeError(desc=('Invalid bgp conn. retry time '
                                    'configuration value %s' %
                                    bgp_conn_retry_time))

    if bgp_conn_retry_time < 10:
        raise ConfigValueError(desc=('Invalid bgp connection retry time'
                                     ' configuration value %s' %
                                     bgp_conn_retry_time))
    return bgp_conn_retry_time


@validate(name=MAX_PATH_EXT_RTFILTER_ALL)
def validate_max_path_ext_rtfilter_all(max_path_ext_rtfilter_all):
    if not isinstance(max_path_ext_rtfilter_all, bool):
        raise ConfigTypeError(desc=('Invalid max_path_ext_rtfilter_all'
                                    ' configuration value %s' %
                                    max_path_ext_rtfilter_all))
    return max_path_ext_rtfilter_all


@validate(name=LOCAL_PREF)
def validate_local_pref(local_pref):
    if not isinstance(local_pref, numbers.Integral):
        raise ConfigTypeError(desc=('Invalid local_pref'
                                    ' configuration value %s' %
                                    local_pref))
    return local_pref


class CommonConf(BaseConf):
    """Encapsulates configurations applicable to all peer sessions.

    Currently if any of these configurations change, it is assumed that current
    active peer session will be bought down and restarted.
    """
    CONF_CHANGED_EVT = 1

    VALID_EVT = frozenset([CONF_CHANGED_EVT])

    REQUIRED_SETTINGS = frozenset([ROUTER_ID, LOCAL_AS])

    OPTIONAL_SETTINGS = frozenset([REFRESH_STALEPATH_TIME,
                                   REFRESH_MAX_EOR_TIME, LABEL_RANGE,
                                   BGP_SERVER_HOSTS, BGP_SERVER_PORT,
                                   TCP_CONN_TIMEOUT,
                                   BGP_CONN_RETRY_TIME,
                                   MAX_PATH_EXT_RTFILTER_ALL,
                                   ALLOW_LOCAL_AS_IN_COUNT,
                                   CLUSTER_ID,
                                   LOCAL_PREF])

    def __init__(self, **kwargs):
        super(CommonConf, self).__init__(**kwargs)

    def _init_opt_settings(self, **kwargs):
        super(CommonConf, self)._init_opt_settings(**kwargs)
        self._settings[ALLOW_LOCAL_AS_IN_COUNT] = compute_optional_conf(
            ALLOW_LOCAL_AS_IN_COUNT, 0, **kwargs)
        self._settings[LABEL_RANGE] = compute_optional_conf(
            LABEL_RANGE, DEFAULT_LABEL_RANGE, **kwargs)
        self._settings[REFRESH_STALEPATH_TIME] = compute_optional_conf(
            REFRESH_STALEPATH_TIME, DEFAULT_REFRESH_STALEPATH_TIME, **kwargs)
        self._settings[REFRESH_MAX_EOR_TIME] = compute_optional_conf(
            REFRESH_MAX_EOR_TIME, DEFAULT_REFRESH_MAX_EOR_TIME, **kwargs)
        self._settings[BGP_SERVER_HOSTS] = compute_optional_conf(
            BGP_SERVER_HOSTS, DEFAULT_BGP_SERVER_HOSTS, **kwargs)
        self._settings[BGP_SERVER_PORT] = compute_optional_conf(
            BGP_SERVER_PORT, DEFAULT_BGP_SERVER_PORT, **kwargs)
        self._settings[TCP_CONN_TIMEOUT] = compute_optional_conf(
            TCP_CONN_TIMEOUT, DEFAULT_TCP_CONN_TIMEOUT, **kwargs)
        self._settings[BGP_CONN_RETRY_TIME] = compute_optional_conf(
            BGP_CONN_RETRY_TIME, DEFAULT_BGP_CONN_RETRY_TIME, **kwargs)
        self._settings[MAX_PATH_EXT_RTFILTER_ALL] = compute_optional_conf(
            MAX_PATH_EXT_RTFILTER_ALL, DEFAULT_MAX_PATH_EXT_RTFILTER_ALL,
            **kwargs)
        self._settings[CLUSTER_ID] = compute_optional_conf(
            CLUSTER_ID, kwargs[ROUTER_ID], **kwargs)
        self._settings[LOCAL_PREF] = compute_optional_conf(
            LOCAL_PREF, DEFAULT_LOCAL_PREF, **kwargs)

    # =========================================================================
    # Required attributes
    # =========================================================================

    @property
    def local_as(self):
        return self._settings[LOCAL_AS]

    @property
    def router_id(self):
        return self._settings[ROUTER_ID]

    # =========================================================================
    # Optional attributes with valid defaults.
    # =========================================================================
    @property
    def cluster_id(self):
        return self._settings[CLUSTER_ID]

    @property
    def allow_local_as_in_count(self):
        return self._settings[ALLOW_LOCAL_AS_IN_COUNT]

    @property
    def bgp_conn_retry_time(self):
        return self._settings[BGP_CONN_RETRY_TIME]

    @property
    def tcp_conn_timeout(self):
        return self._settings[TCP_CONN_TIMEOUT]

    @property
    def refresh_stalepath_time(self):
        return self._settings[REFRESH_STALEPATH_TIME]

    @property
    def refresh_max_eor_time(self):
        return self._settings[REFRESH_MAX_EOR_TIME]

    @property
    def label_range(self):
        return self._settings[LABEL_RANGE]

    @property
    def bgp_server_hosts(self):
        return self._settings[BGP_SERVER_HOSTS]

    @property
    def bgp_server_port(self):
        return self._settings[BGP_SERVER_PORT]

    @property
    def max_path_ext_rtfilter_all(self):
        return self._settings[MAX_PATH_EXT_RTFILTER_ALL]

    @property
    def local_pref(self):
        return self._settings[LOCAL_PREF]

    @classmethod
    def get_opt_settings(self):
        self_confs = super(CommonConf, self).get_opt_settings()
        self_confs.update(CommonConf.OPTIONAL_SETTINGS)
        return self_confs

    @classmethod
    def get_req_settings(self):
        self_confs = super(CommonConf, self).get_req_settings()
        self_confs.update(CommonConf.REQUIRED_SETTINGS)
        return self_confs

    @classmethod
    def get_valid_evts(self):
        self_valid_evts = super(CommonConf, self).get_valid_evts()
        self_valid_evts.update(CommonConf.VALID_EVT)
        return self_valid_evts

    def update(self, **kwargs):
        """Updates global configuration settings with given values.

        First checks if given configuration values differ from current values.
        If any of the configuration values changed, generates a change event.
        Currently we generate change event for any configuration change.
        Note: This method is idempotent.
        """
        # Update inherited configurations
        super(CommonConf, self).update(**kwargs)
        conf_changed = False

        # Validate given configurations and check if value changed
        for conf_name, conf_value in kwargs.items():
            rtconf.base.get_validator(conf_name)(conf_value)
            item1 = self._settings.get(conf_name, None)
            item2 = kwargs.get(conf_name, None)

            if item1 != item2:
                conf_changed = True

        # If any configuration changed, we update configuration value and
        # notify listeners
        if conf_changed:
            for conf_name, conf_value in kwargs.items():
                # Since all new values are already validated, we can use them
                self._settings[conf_name] = conf_value

            self._notify_listeners(CommonConf.CONF_CHANGED_EVT, self)


class CommonConfListener(BaseConfListener):
    """Base listener for various changes to common configurations."""

    def __init__(self, global_conf):
        super(CommonConfListener, self).__init__(global_conf)
        global_conf.add_listener(CommonConf.CONF_CHANGED_EVT,
                                 self.on_update_common_conf)

    def on_update_common_conf(self, evt):
        raise NotImplementedError('This method should be overridden.')
