"""
Dynamic Routing can be enabled on devices configured in engine/VPN mode.
Configuring dynamic routing consists of enabling the routing protocol
on the engine and adding the routing elements on the interfaces at the
engine routing level.

For adding OSPF configurations, several steps are required:

* Enable OSPF on the engine and specific the OSPF Profile
* Create or locate an existing OSPFArea to be used
* Modify the routing interface and add the OSPFArea

Enable OSPF on an existing engine using the default OSPF system profile::

    engine.ospf.enable()

Create an OSPFArea using the default OSPF Interface Setting profile::

    OSPFArea.create(name='customOSPFArea')

Add OSPF area to an interface routing configuration (add to nicid '0')::

    interface = engine.routing.get(0)
    interface.add_ospf_area(area)

Disable OSPF on an engine::

    engine.ospf.disable()

Finding profiles or elements can also be done through collections::

    >>> list(OSPFProfile.objects.all())
    [OSPFProfile(name=Default OSPFv2 Profile)]

    >>> list(OSPFArea.objects.all())
    [OSPFArea(name=area0)]

The OSPF relationship can be represented as::

    Engine --uses an--> OSPF Profile --has-a--> OSPF Domain Setting
    Engine Routing --uses-an--> OSPF Area --has-a--> OSPF Interface Setting

Only Layer3Firewall and Layer3VirtualEngine types can support running OSPF.

.. seealso:: :py:class:`smc.core.engines.Layer3Firewall` and
             :py:class:`smc.core.engines.Layer3VirtualEngine`

"""
from smc.base.model import Element, ElementCreator, ElementCache, ElementRef, ElementList
from smc.base.util import element_resolver


class OSPF(object):
    """
    OSPF configuration on the engine. Access through an engine reference::

        engine.dynamic_routing.ospf.status
        engine.dynamic_rotuing.ospf.enable(....)

    When making changes to the OSPF configuration, any methods
    called that change the configuration also require that
    engine.update() is called once changes are complete. This way
    you can make multiple changes without refreshing the engine cache.

    :ivar OSPFProfile profile: OSPFProfile reference for this engine
    """

    profile = ElementRef("ospfv2_profile_ref")

    def __init__(self, data=None):
        self.data = data if data else ElementCache()

    @property
    def router_id(self):
        """
        Get the router ID for this OSPF configuration. If None, then
        the ID will use the interface IP.

        :return: str or None
        """
        return self.data.get("router_id")

    @property
    def status(self):
        """
        Is OSPF enabled on this engine.

        :rtype: bool
        """
        return self.data.get("enabled")

    def disable(self):
        """
        Disable OSPF on this engine.

        :return: None
        """
        self.data.update(enabled=False)

    def enable(self, ospf_profile=None, router_id=None):
        """
        Enable OSPF on this engine. For master engines, enable
        OSPF on the virtual firewall.

        Once enabled on the engine, add an OSPF area to an interface::

            engine.dynamic_routing.ospf.enable()
            interface = engine.routing.get(0)
            interface.add_ospf_area(OSPFArea('myarea'))

        :param str,OSPFProfile ospf_profile: OSPFProfile element or str
            href; if None, use default profile
        :param str router_id: single IP address router ID
        :raises ElementNotFound: OSPF profile not found
        :return: None
        """
        ospf_profile = (
            element_resolver(ospf_profile)
            if ospf_profile
            else OSPFProfile("Default OSPFv2 Profile").href
        )

        self.data.update(enabled=True, ospfv2_profile_ref=ospf_profile, router_id=router_id)

    def update_configuration(self, **kwargs):
        """
        Update the OSPF configuration using kwargs that match the
        `enable` constructor.

        :param dict kwargs: keyword arguments matching enable constructor.
        :return: whether change was made
        :rtype: bool
        """
        updated = False
        if "ospf_profile" in kwargs:
            kwargs.update(ospfv2_profile_ref=kwargs.pop("ospf_profile"))
        for name, value in kwargs.items():
            _value = element_resolver(value)
            if self.data.get(name) != _value:
                self.data[name] = _value
                updated = True
        return updated


class OSPFArea(Element):
    """
    OSPF Area is an element that identifies general settings for an
    OSPF configuration applied to an engine routing node. The OSPFArea
    has a reference to an OSPFInterfaceSetting and is required when
    creating.

    Create a basic OSPFArea with just area id::

        OSPFArea.create(name='myarea', area_id=0)

    Create an OSPFArea and use a custom OSPFInterfaceSetting element::

        OSPFArea.create(
            name='customOSPFArea',
            interface_settings_ref=OSPFInterfaceSetting('myospf'),
            area_id=3)

    **Advanced example:**

    Adding ospf_virtual_links_endpoints::

        OSPFArea.create(
            name='ospf',
            interface_settings_ref=intf,
            area_id=3,
            ospfv2_virtual_links_endpoints_container=
                [{'interface_settings_ref':
                    'http://172.18.1.150:8082/6.1/elements/ospfv2_interface_settings/8',
                  'router_id_endpoint_A': '192.168.1.1',
                  'router_id_endpoint_B': '192.168.1.254'},
                 {'router_id_endpoint_A': '172.18.1.254',
                  'router_id_endpoint_B': '172.18.1.200'}])

    When using ABR substitute rules, there are 3 actions, 'aggregate', 'not_advertise'
    and 'substitute_with'. All references required are of type
    :py:class:`smc.elements.network.Network`. These elements can either be created or
    retrieved using collections, or by getting the resource directly.

    Example of creating an OSPF area and using ABR settings::

        OSPFArea.create(
                name='area_with_abr',
                interface_settings_ref=intf,
                area_id=1,
                ospf_abr_substitute_container=[
                    {'subnet_ref': 'http://172.18.1.150:8082/6.1/elements/network/143',
                     'substitute_ref': 'http://172.18.1.150:8082/6.1/elements/network/1547',
                     'substitute_type': 'substitute_with'},
                    {'subnet_ref': 'http://172.18.1.150:8082/6.1/elements/network/979',
                     'substitute_type': 'aggregate'}])

    :ivar OSPFInterfaceSetting interface_settings_ref: reference to the OSPFInterfaceSetting
    :ivar list(IPPrefixList,IPAccessList) inbound_filters: Inbound filters attached to this
        OSPF Area.
    :ivar list(IPPrefixList,IPAccessList) outbound_filters: Outbound filter attached to this
        OSPF Area.
    """

    typeof = "ospfv2_area"
    interface_settings_ref = ElementRef("interface_settings_ref")
    inbound_filters = ElementList("inbound_filters_ref")
    outbound_filters = ElementList("outbound_filters_ref")

    @classmethod
    def create(
        cls,
        name,
        interface_settings_ref=None,
        area_id=1,
        area_type="normal",
        outbound_filters=None,
        inbound_filters=None,
        shortcut_capable_area=False,
        ospfv2_virtual_links_endpoints_container=None,
        ospf_abr_substitute_container=None,
        comment=None,
        **kwargs
    ):
        """
        Create a new OSPF Area

        :param str name: name of OSPFArea configuration
        :param str,OSPFInterfaceSetting interface_settings_ref: an OSPFInterfaceSetting
            element or href. If None, uses the default system profile
        :param str name: area id
        :param str area_type: \\|normal\\|stub\\|not_so_stubby\\|totally_stubby\\|
               totally_not_so_stubby
        :param list outbound_filters: reference to an IPAccessList and or IPPrefixList.
            You can only have one outbound prefix or access list
        :param list inbound_filters: reference to an IPAccessList and or IPPrefixList.
            You can only have one outbound prefix or access list
        :param shortcut_capable_area: True|False
        :param list ospfv2_virtual_links_endpoints_container: virtual link endpoints
        :param list ospf_abr_substitute_container: substitute types:
               \\|aggregate\\|not_advertise\\|substitute_with
        :param str comment: optional comment
        :raises CreateElementFailed: failed to create with reason
        :rtype: OSPFArea
        """
        interface_settings_ref = (
            element_resolver(interface_settings_ref)
            or OSPFInterfaceSetting("Default OSPFv2 Interface Settings").href
        )

        if "inbound_filters_ref" in kwargs:
            inbound_filters = kwargs.get("inbound_filters_ref")

        if "outbound_filters_ref" in kwargs:
            outbound_filters = kwargs.get("outbound_filters_ref")

        json = {
            "name": name,
            "area_id": area_id,
            "area_type": area_type,
            "comment": comment,
            "inbound_filters_ref": element_resolver(inbound_filters),
            "interface_settings_ref": interface_settings_ref,
            "ospf_abr_substitute_container": ospf_abr_substitute_container,
            "ospfv2_virtual_links_endpoints_container": ospfv2_virtual_links_endpoints_container,
            "outbound_filters_ref": element_resolver(outbound_filters),
            "shortcut_capable_area": shortcut_capable_area,
        }

        return ElementCreator(cls, json)

    @classmethod
    def update_or_create(cls, with_status=False, **kwargs):
        if "inbound_filters" in kwargs:
            kwargs.update(inbound_filters_ref=element_resolver(kwargs.pop("inbound_filters")))
        if "outbound_filters" in kwargs:
            kwargs.update(outbound_filters_ref=element_resolver(kwargs.pop("outbound_filters")))
        return super(OSPFArea, cls).update_or_create(with_status=with_status, **kwargs)


class OSPFInterfaceSetting(Element):
    """
    OSPF Interface Setting indicate specific configurations that are
    applied to the interface and OSPF Area configuration, including
    authentication.

    If you require non-default settings applied to your interface
    OSPF instance, you can create a custom interface profile::

        OSPFInterfaceSetting.create(
            name='myprofile',
            dead_interval=30,
            hello_interval=5)

    When using authentication on interface settings, there are two types,
    password authentication (plain text) or message digest.

    When specifying an authentication_type='password', the password parameter
    must be provided.

    When specifying authentication_type='message_digest', the key_chain_ref
    parameter must be specified.
    """

    typeof = "ospfv2_interface_settings"
    key_chain_ref = ElementRef("key_chain_ref")

    @classmethod
    def create(
        cls,
        name,
        dead_interval=40,
        hello_interval=10,
        hello_interval_type="normal",
        dead_multiplier=1,
        mtu_mismatch_detection=True,
        retransmit_interval=5,
        router_priority=1,
        transmit_delay=1,
        authentication_type=None,
        password=None,
        key_chain_ref=None,
    ):
        """
        Create custom OSPF interface settings profile

        :param str name: name of interface settings
        :param int dead_interval: in seconds
        :param str hello_interval: in seconds
        :param str hello_interval_type: \\|normal\\|fast_hello
        :param int dead_multipler: fast hello packet multipler
        :param bool mtu_mismatch_detection: True|False
        :param int retransmit_interval: in seconds
        :param int router_priority: set priority
        :param int transmit_delay: in seconds
        :param str authentication_type: \\|password\\|message_digest
        :param str password: max 8 chars (required when
               authentication_type='password')
        :param str,Element key_chain_ref: OSPFKeyChain (required when
               authentication_type='message_digest')
        :raises CreateElementFailed: create failed with reason
        :return: instance with meta
        :rtype: OSPFInterfaceSetting
        """
        json = {
            "name": name,
            "authentication_type": authentication_type,
            "password": password,
            "key_chain_ref": element_resolver(key_chain_ref),
            "dead_interval": dead_interval,
            "dead_multiplier": dead_multiplier,
            "hello_interval": hello_interval,
            "hello_interval_type": hello_interval_type,
            "mtu_mismatch_detection": mtu_mismatch_detection,
            "retransmit_interval": retransmit_interval,
            "router_priority": router_priority,
            "transmit_delay": transmit_delay,
        }

        return ElementCreator(cls, json)


class OSPFKeyChain(Element):
    """
    OSPF Key Chain is used for authenticating OSPFv2 packets. If required,
    create a key chain and specify authentication in the OSPFInterfaceSetting
    referencing this element.

    Is message-digest authentication is required on an OSPFInterfaceSetting,
    first create the key chain and use the reference to create the ospf
    interface profile::

        key_chain = OSPFKeyChain('secure-keychain') #obtain resource
        OSPFInterfaceSetting.create(
            name='authenicated-ospf',
            authentication_type='message_digest',
            key_chain_ref=key_chain.href)
    """

    typeof = "ospfv2_key_chain"

    @classmethod
    def create(cls, name, key_chain_entry):
        """
        Create a key chain with list of keys

        Key_chain_entry format is::

            [{'key': 'xxxx', 'key_id': 1-255, 'send_key': True|False}]

        :param str name: Name of key chain
        :param list key_chain_entry: list of key chain entries
        :raises CreateElementFailed: create failed with reason
        :return: instance with meta
        :rtype: OSPFKeyChain
        """
        key_chain_entry = key_chain_entry or []
        json = {"name": name, "ospfv2_key_chain_entry": key_chain_entry}

        return ElementCreator(cls, json)


def _format_redist_entry(redistribution_entry):
    for entry in redistribution_entry:
        _filter = entry.pop("filter", None)
        if _filter and _filter.typeof == "ip_access_list":
            entry.update(filter_type="access_list", redistribution_filter_ref=_filter.href)
        elif _filter and _filter.typeof == "route_map":
            entry.update(filter_type="route_map_policy", redistribution_rm_ref=_filter.href)
        if "metric_type" not in entry:
            entry.update(metric_type="external_1")
    return redistribution_entry


class OSPFProfile(Element):
    """
    An OSPF Profile contains administrative distance and redistribution
    settings. An OSPF Profile is set on the engine element when enabling
    OSPF.

    These settings are always in effect:

    * No autosummary

    Example of creating an OSPFProfile with the default domain profile::

        OSPFProfile.create(name='myospf')

    .. note:: Enable OSPF on engine using engine.ospf.enable()

    :ivar int external_distance: external distance metric
    :ivar int inter_distance: inter distance metric
    :ivar int intra_distance: intra distance metric
    :ivar int default_metric: set a default metric for all unset areas
    :ivar list redistribution_entry: settings for static, connected, etc
    :ivar OSPFDomainSetting domain_settings_ref: OSPF Domain Settings profile
        used for this OSPF Profile
    """

    typeof = "ospfv2_profile"
    domain_settings_ref = ElementRef("domain_settings_ref")

    @classmethod
    def create(
        cls,
        name,
        domain_settings_ref=None,
        external_distance=110,
        inter_distance=110,
        intra_distance=110,
        redistribution_entry=None,
        default_metric=None,
        comment=None,
    ):
        """
        Create an OSPF Profile.

        If providing a list of redistribution entries, provide in the following
        dict format:

        {'enabled': boolean, 'metric_type': 'external_1' or 'external_2',
         'metric': 2, 'type': 'kernel'}

        Valid types for redistribution entries are: kernel, static, connected, bgp,
        and default_originate.

        You can also provide a 'filter' key with either an IPAccessList or RouteMap
        element to use for further access control on the redistributed route type.
        If metric_type is not provided, external_1 (E1) will be used.

        An example of a redistribution_entry would be::

            {u'enabled': True,
             u'metric': 123,
             u'metric_type': u'external_2',
             u'filter': RouteMap('myroutemap'),
             u'type': u'static'}

        :param str name: name of profile
        :param str,OSPFDomainSetting domain_settings_ref: OSPFDomainSetting
            element or href
        :param int external_distance: route metric (E1-E2)
        :param int inter_distance: routes learned from different areas (O IA)
        :param int intra_distance: routes learned from same area (O)
        :param list redistribution_entry: how to redistribute the OSPF routes.
        :raises CreateElementFailed: create failed with reason
        :rtype: OSPFProfile
        """
        json = {
            "name": name,
            "external_distance": external_distance,
            "inter_distance": inter_distance,
            "intra_distance": intra_distance,
            "default_metric": default_metric,
            "comment": comment,
        }

        if redistribution_entry:
            json.update(redistribution_entry=_format_redist_entry(redistribution_entry))

        domain_settings_ref = (
            element_resolver(domain_settings_ref)
            or OSPFDomainSetting("Default OSPFv2 Domain Settings").href
        )

        json.update(domain_settings_ref=domain_settings_ref)

        return ElementCreator(cls, json)

    @classmethod
    def update_or_create(cls, filter_key=None, with_status=False, **kwargs):
        if "redistribution_entry" in kwargs:
            kwargs.update(
                redistribution_entry=_format_redist_entry(kwargs.pop("redistribution_entry", []))
            )

        element, updated, created = super(OSPFProfile, cls).update_or_create(
            filter_key, defer_update=True, **kwargs
        )
        if not created:
            # Deferred update, update redistribution settings
            if "redistribution_entry" in kwargs:
                element.data["redistribution_entry"] = kwargs.get("redistribution_entry")
                element.update()
                updated = True

        if with_status:
            return element, updated, created
        return element


class OSPFDomainSetting(Element):
    """
    An OSPF Domain Setting provides settings for area border router (ABR)
    type, throttle timer settings, and the max metric router link-state
    advertisement (LSA) settings.

    An OSPF Profile requires a reference to an OSPF Domain Setting.

    Create a custom OSPF Domain Setting element::

        OSPFDomainSetting.create(
            name='mydomain',
            abr_type='standard',
            auto_cost_bandwidth=200,
            deprecated_algorithm=True)
    """

    typeof = "ospfv2_domain_settings"

    @classmethod
    def create(
        cls,
        name,
        abr_type="cisco",
        auto_cost_bandwidth=100,
        deprecated_algorithm=False,
        initial_delay=200,
        initial_hold_time=1000,
        max_hold_time=10000,
        shutdown_max_metric_lsa=0,
        startup_max_metric_lsa=0,
    ):
        """
        Create custom Domain Settings

        Domain settings are referenced by an OSPFProfile

        :param str name: name of custom domain settings
        :param str abr_type: cisco|shortcut|standard
        :param int auto_cost_bandwidth: Mbits/s
        :param bool deprecated_algorithm: RFC 1518 compatibility
        :param int initial_delay: in milliseconds
        :param int initial_hold_type: in milliseconds
        :param int max_hold_time: in milliseconds
        :param int shutdown_max_metric_lsa: in seconds
        :param int startup_max_metric_lsa: in seconds
        :raises CreateElementFailed: create failed with reason
        :return: instance with meta
        :rtype: OSPFDomainSetting
        """
        json = {
            "name": name,
            "abr_type": abr_type,
            "auto_cost_bandwidth": auto_cost_bandwidth,
            "deprecated_algorithm": deprecated_algorithm,
            "initial_delay": initial_delay,
            "initial_hold_time": initial_hold_time,
            "max_hold_time": max_hold_time,
            "shutdown_max_metric_lsa": shutdown_max_metric_lsa,
            "startup_max_metric_lsa": startup_max_metric_lsa,
        }

        return ElementCreator(cls, json)
