"""
Module provides an interface to sub-interfaces on an engine. A 'top level' interface
is linked from the engine and will be PhysicalInterface, TunnelInterface, etc. Within
the top level interface, there are sub-interface configurations that identify the
basic settings such as ip address, network, administrative settings etc. These are
not called directly but used as a reference to the top level interface.
All sub interfaces are type dict.
"""

from smc.base.structs import NestedDict, BaseIterable
from smc.api.exceptions import EngineCommandFailed


def get_sub_interface(typeof):
    if typeof in clsmembers:
        return clsmembers[typeof]


class SubInterfaceCollection(BaseIterable):
    """
    A Sub Interface collection for non-VLAN interfaces.
    """

    def __init__(self, interface):
        data = [
            clsmembers.get(kind)(data)
            for intf in interface.data.get("interfaces", [])
            for kind, data in intf.items()
        ]
        super(SubInterfaceCollection, self).__init__(data)


class SubInterface(NestedDict):
    def __init__(self, data):
        super(SubInterface, self).__init__(data=data)

    def change_interface_id(self, interface_id):
        """
        Generic change interface ID for VLAN interfaces that are not
        Inline Interfaces (non-VLAN sub interfaces do not have an
        interface_id field).

        :param str, int interface_id: interface ID value
        """
        _, second = self.nicid.split(".")
        self.update(nicid="{}.{}".format(str(interface_id), second))

    def change_vlan_id(self, vlan_id):
        """
        Change a VLAN id

        :param str vlan_id: new vlan
        """
        first, _ = self.nicid.split(".")
        self.update(nicid="{}.{}".format(first, str(vlan_id)))

    def __getattr__(self, key):
        return self.get(key)


class ClusterVirtualInterface(SubInterface):
    """
    These interfaces (CVI) are used on cluster devices and applied to layer 3
    interfaces. They specify a 'VIP' (or shared IP) to be used for traffic load
    balancing or high availability. Each engine will still also have a 'node'
    interface for communication to/from the engine itself.
    The following getter/setter properties are available:

    :ivar str address: address of the CVI
    :ivar boolean auth_request: interface for authentication requests (only 1)
    :ivar str network_value: network address for interface, i.e. 1.1.1.0/24
    :ivar int nicid: nic interface identifier
    :ivar boolean relayed_by_dhcp: is the interface using DHCP
    :ivar str igmp_mode: IGMP mode (upstream/downstream/None)

    """

    typeof = "cluster_virtual_interface"

    def __init__(self, data):
        super(ClusterVirtualInterface, self).__init__(data)

    @classmethod
    def create(cls, interface_id, address, network_value, **kwargs):
        """
        :param int interface_id: nic id to use for physical interface
        :param str address: address of CVI
        :param str network_value: network for CVI
        :rtype: ClusterVirtualInterface
        """
        data = {
            "address": address,
            "network_value": network_value,
            "nicid": str(interface_id),
            "auth_request": False,
        }

        for k, v in kwargs.items():
            data.update({k: v})

        return cls(data)

    @property
    def vlan_id(self):
        """
        VLAN ID for this interface, if any

        :return: VLAN identifier
        :rtype: str
        """
        nicid = self.nicid
        if nicid:
            v = nicid.split(".")
            if len(v) > 1:
                return nicid.split(".")[1]

    def __repr__(self):
        if self.vlan_id:
            return "{0}(address={1}, vlan_id={2})".format(
                self.__class__.__name__, self.address, self.vlan_id
            )
        else:
            return "{0}(address={1})".format(self.__class__.__name__, self.address)


class InlineInterface(SubInterface):
    """
    InlineInterface
    This interface type is used on layer 2 or IPS related engines. It requires
    that you specify two interfaces to be part of the inline pair. These interfaces
    do not need to be sequential. It is also possible to add VLANs and zones to the
    inline interfaces.
    The logical interface reference needs to be unique for inline and capture interfaces
    when they are applied on the same engine.

    :ivar boolean inspect_unspecified_vlans: promiscuous SPAN on unspecified VLANs
    :ivar str logical_interface_ref (required): logical interface to use, by href
    :ivar str failure_mode: normal or bypass
    :ivar str nicid: interfaces for inline pair, for example, '4.50-5.55', '5-6'
    :ivar str vlan_id: vlan identifier for interface
    :ivar str zone_ref (optional): zone for second interface in pair
    """

    typeof = "inline_interface"

    def __init__(self, data):
        super(InlineInterface, self).__init__(data)

    @classmethod
    def create(
        cls, interface_id, logical_interface_ref, second_interface_id=None, zone_ref=None, **kwargs
    ):
        """
        :param str interface_id: two interfaces, i.e. '1-2', '4-5', '7-10', etc
        :param str logical_ref: logical interface reference
        :param str zone_ref: reference to zone, set on second inline pair
        :rtype: dict
        """
        data = {
            "inspect_unspecified_vlans": True,
            "nicid": "{}-{}".format(str(interface_id), str(second_interface_id))
            if second_interface_id
            else str(interface_id),
            "logical_interface_ref": logical_interface_ref,
            "zone_ref": zone_ref,
        }

        for k, v in kwargs.items():
            data.update({k: v})

        return cls(data)

    def change_vlan_id(self, vlan_id):
        """
        Change a VLAN id for an inline interface.

        :param str vlan_id: New VLAN id. Can be in format '1-2' or
            a single numerical value. If in '1-2' format, this specifies
            the vlan ID for the first inline interface and the rightmost
            for the second.
        :return: None
        """
        first, second = self.nicid.split("-")
        firstintf = first.split(".")[0]
        secondintf = second.split(".")[0]
        newvlan = str(vlan_id).split("-")
        self.update(nicid="{}.{}-{}.{}".format(firstintf, newvlan[0], secondintf, newvlan[-1]))

    def change_interface_id(self, newid):
        """
        Change the inline interface ID. The current format is
        nicid='1-2', where '1' is the top level interface ID (first),
        and '2' is the second interface in the pair. Consider the existing
        nicid in case this is a VLAN.

        :param str newid: string defining new pair, i.e. '3-4'
        :return: None
        """
        try:
            newleft, newright = newid.split("-")
        except ValueError:
            raise EngineCommandFailed(
                "You must provide two parts when changing "
                "the interface ID on an inline interface, i.e. 1-2."
            )
        first, second = self.nicid.split("-")
        if "." in first and "." in second:
            firstvlan = first.split(".")[-1]
            secondvlan = second.split(".")[-1]
            self.update(nicid="{}.{}-{}.{}".format(newleft, firstvlan, newright, secondvlan))
        else:
            # Top level interface or no VLANs
            self.update(nicid=newid)

    @property
    def vlan_id(self):
        """
        VLAN ID for this interface, if any

        :return: VLAN identifier
        :rtype: str
        """
        nicids = self.nicid.split("-")
        if nicids:
            u = []
            for vlan in nicids:
                if vlan.split(".")[-1] not in u:
                    u.append(vlan.split(".")[-1])
            return "-".join(u)

    def __repr__(self):
        return "{0}(nicid={1})".format(self.__class__.__name__, self.nicid)


class InlineIPSInterface(InlineInterface):
    """
    An Inline IPS Interface is a new interface type introduced
    in SMC version 6.3. This interface type is the same as a
    normal IPS interface except that it is applied on a Layer 3
    Firewall.

    .. versionadded:: 0.5.6
        Requires SMC 6.3.
    """

    typeof = "inline_ips_interface"

    def __init__(self, data):
        super(InlineIPSInterface, self).__init__(data)


class InlineL2FWInterface(InlineInterface):
    """
    An Inline L2FW Interface is a new interface type introduced
    in SMC version 6.3. This interface type is the a layer 2 engine
    interface on a layer 3 firewall. By default this interface
    type does not support bypass mode and will discard on overload.

    .. versionadded:: 0.5.6
        Requires SMC 6.3.
    """

    typeof = "inline_l2fw_interface"

    def __init__(self, data):
        super(InlineL2FWInterface, self).__init__(data)

    @classmethod
    def create(
        cls, interface_id, logical_interface_ref, second_interface_id=None, zone_ref=None, **kwargs
    ):
        kwargs.pop("failure_mode", None)
        return super(InlineL2FWInterface, cls).create(
            interface_id, logical_interface_ref, second_interface_id, zone_ref=zone_ref, **kwargs
        )


class CaptureInterface(SubInterface):
    """
    Capture Interface (SPAN)
    This is a single physical interface type that can be installed on either
    layer 2 or IPS engine roles. It enables the NGFW to capture traffic on
    the wire without actually blocking it (although blocking is possible).

    :ivar boolean inspect_unspecified_vlans: promiscuous SPAN on unspecified VLANs
    :ivar str logical_interface_ref (required): logical interface to use, by href
    :ivar int reset_interface_nicid: if sending passive RST back, interface id to use
    :ivar str,int nicid: nicid for this capture interface
    """

    typeof = "capture_interface"

    def __init__(self, data):
        super(CaptureInterface, self).__init__(data)

    @classmethod
    def create(cls, interface_id, logical_interface_ref, **kw):
        """
        :param int interface_id: the interface id
        :param str logical_ref: logical interface reference, must be unique from
               inline intfs
        :rtype: dict
        """
        data = {
            "inspect_unspecified_vlans": True,
            "logical_interface_ref": logical_interface_ref,
            "nicid": str(interface_id),
        }

        if "reset_interface_nicid" in kw:
            data.update(reset_interface_nicid=kw.get("reset_interface_nicid"))

        return cls(data)

    @property
    def vlan_id(self):
        if "." in self.nicid:
            return self.nicid.split(".")[-1]

    def __repr__(self):
        return "{0}(nicid={1})".format(self.__class__.__name__, self.nicid)


class NodeInterface(SubInterface):
    """
    Node Interface
    Node dedicated interface (NDI) is used on specific engine types and represents
    an interface used for management (IPS and layer 2 engines), or as normal layer
    3 interfaces such as on a layer 3 firewall cluster.

    For Layer 2 Firewall/IPS these are used as individual interfaces. On clusters,
    these are used to define the node specific address for each node member, along
    with a cluster virtual interface.

    :ivar str address: ip address of this interface
    :ivar str network_value: network for this interface, i.e. 1.1.1.0/24
    :ivar str or int nicid: nic interface id
    :ivar int nodeid: node identifier for interface (in a cluster, each node will be unique)
    :ivar boolean outgoing: This option defines the IP address that the nodes use if they
        have to initiate connections (system communications, ping, etc.) through an interface that
        has no Node Dedicated IP Address. In Firewall Clusters, you must select an interface
         that has an IP address defined for all nodes.
    :ivar boolean primary_heartbeat: Whether interface is the primary heartbeat interface for
        communications between the nodes. It is recommended that you use a Physical Interface, not
        a VLAN Interface. It is also recommended that you do not direct any other traffic
        through this interface.
    :ivar boolean primary_mgt: Is it the Primary Control Interface for Management Server contact.
        There must be one and only one Primary Control Interface
    :ivar boolean auth_request: whether to specify this interface as interface for authentication
        requests. Should be set on interface acting as management
    :ivar boolean auth_request_source: If the authentication requests are sent to an external
        authentication server over VPN, select an interface with a Node Dedicated IP address that
        you want use for the authentication requests
    :ivar boolean reverse_connection: Reverse connection enables engine to contact SMC
        versus other way around
    :ivar str vlan_id: VLAN id for interface if assigned
    :ivar boolean backup_mgt: Whether interface is a backup control interface that is used if the
        primary control interface is not available
    :ivar boolean backup_heartbeat: Whether the interface is a backup heartbeat.
        It is not mandatory to configure a backup heartbeat interface.
    :ivar boolean dynamic: Whether this is a DHCP interface
    :ivar int dynamic_index: The dynamic index of the DHCP interface. The value is between
        1 and 16. Only used when 'dynamic' is set to True.
    :ivar str igmp_mode: IGMP mode (upstream/downstream/None)
    :ivar boolean vrrp: Enable VRRP
    :ivar str vrrp_address: IP address if VRRP is enabled
    :ivar int vrrp_id: The VRRP ID. Required only for VRRP mode
    :ivar int vrrp_priority: The VRRP Priority. Required only for VRRP mode
    """

    typeof = "node_interface"

    def __init__(self, data):
        super(NodeInterface, self).__init__(data)

    @classmethod
    def create(cls, interface_id, address, network_value, nodeid=1, **kwargs):
        """
        :param int interface_id: interface id
        :param str address: ip address of the interface
        :param str network_value: network/netmask, i.e. x.x.x.x/24
        :param int nodeid: for clusters, used to identify the node number
        :rtype: dict
        """
        data = {
            "address": address,
            "network_value": network_value,
            "nicid": str(interface_id),
            "auth_request": False,
            "backup_heartbeat": False,
            "nodeid": nodeid,
            "outgoing": False,
            "primary_mgt": False,
            "primary_heartbeat": False,
        }

        for k, v in kwargs.items():
            data.update({k: v})

        return cls(data)

    @property
    def vlan_id(self):
        """
        VLAN ID for this interface, if any

        :return: VLAN identifier
        :rtype: str
        """
        nicid = self.nicid
        # if nicid:
        if nicid and not nicid.startswith("SWP_"):  # Ignore switch physical interface
            v = nicid.split(".")
            if len(v) > 1:
                return nicid.split(".")[1]

    def __repr__(self):
        if self.vlan_id:
            return "{0}(address={1}, vlan_id={2})".format(
                self.__class__.__name__, self.address, self.vlan_id
            )
        elif self.dynamic_index:
            return "{0}(address=DHCP, dynamic_index={1})".format(
                self.__class__.__name__, self.dynamic_index
            )
        else:
            return "{0}(address={1})".format(self.__class__.__name__, self.address)


class SingleNodeInterface(NodeInterface):
    """
    SingleNodeInterface
    This interface is used by single node Layer 3 Firewalls. This type of interface
    can be a management interface as well as a non-management routed interface.

    :ivar bool dynamic: is this interface a dynamic DHCP interface
    :ivar int dynamic_index: dynamic interfaces index value
    :ivar boolean automatic_default_route: Flag to know if the dynamic default route will
        be automatically created for this dynamic interface. Used in DHCP interfaces only
    """

    typeof = "single_node_interface"

    def __init__(self, data):
        super(SingleNodeInterface, self).__init__(data)

    @classmethod
    def create(cls, interface_id, address=None, network_value=None, nodeid=1, **kw):
        """
        :param int interface_id: interface id
        :param str address: address of this interface
        :param str network_value: network of this interface in cidr x.x.x.x/24
        :param int nodeid: if a cluster, identifies which node this is for
        :rtype: dict
        """
        data = {
            "address": address,
            "auth_request": False,
            "auth_request_source": False,
            "primary_heartbeat": False,
            "backup_heartbeat": False,
            "backup_mgt": False,
            "dynamic": False,
            "network_value": network_value,
            "nicid": str(interface_id),
            "nodeid": nodeid,
            "outgoing": False,
            "primary_mgt": False,
        }

        for k, v in kw.items():
            data.update({k: v})

        if "dynamic" in kw and kw["dynamic"] is not None:
            for key in ("address", "network_value"):
                data.pop(key, None)
            if data["primary_mgt"]:  # Have to set auth_request to a different interface for DHCP
                data["auth_request"] = False

            if data.get("dynamic_index", None) is None:
                data["dynamic_index"] = 1
            elif data.get("automatic_default_route") is None:
                data.update(automatic_default_route=True)

        return cls(data)


class LoopbackClusterInterface(ClusterVirtualInterface):
    """
    This represents the CVI Loopback IP address.
    A CVI loopback IP address is used for loopback traffic that is sent to
    the whole cluster. It is shared by all the nodes in the cluster.
    """

    typeof = "loopback_cluster_virtual_interface"

    def __init__(self, data, engine=None):
        self._engine = engine
        super(LoopbackClusterInterface, self).__init__(data)

    @classmethod
    def create(cls, address, ospf_area=None, **kwargs):
        """
        Create a loopback interface. Uses parent constructor

        :rtype: LoopbackClusterInterface
        """
        return super(LoopbackClusterInterface, cls).create(
            address=address,
            network_value="{}/32".format(address),
            interface_id="Loopback Interface",
            ospfv2_area_ref=ospf_area,
            **kwargs
        )

    def delete(self):
        """
        Delete a loopback cluster virtual interface from this engine.
        Changes to the engine configuration are done immediately.

        You can find cluster virtual loopbacks by iterating at the
        engine level::

            for loopbacks in engine.loopback_interface:
                ...

        :raises UpdateElementFailed: failure to delete loopback interface
        :return: None
        """
        self._engine.data[self.typeof] = [
            loopback
            for loopback in self._engine.data.get(self.typeof, [])
            if loopback.get("address") != self.address
        ]

        self._engine.update()

    def add_cvi_loopback(self, address, ospf_area=None, **kw):
        """
        Add a loopback interface as a cluster virtual loopback. This enables
        the loopback to 'float' between cluster members. Changes are committed
        immediately.

        :param str address: ip address for loopback
        :param int rank: rank of this entry
        :param str,Element ospf_area: optional ospf_area to add to loopback
        :raises UpdateElementFailed: failure to save loopback address
        :return: None
        """
        lb = self.create(address, ospf_area, **kw)

        if self.typeof in self._engine.data:
            # there are existing entries
            loopbacks = self._engine.data[self.typeof]

            last_rank = 1
            for loopback in loopbacks:
                if loopback["rank"] > last_rank:
                    last_rank = loopback["rank"]
            lb["rank"] = last_rank + 1
            loopbacks.append(lb.data)
        else:
            # it is the first entry
            lb["rank"] = 1
            self._engine.data[self.typeof] = [lb.data]

        self._engine.update()

    def __repr__(self):
        return "LoopbackClusterInterface(address={}, auth_request={})".format(
            self.address, self.auth_request
        )


class LoopbackInterface(NodeInterface):
    """
    Loopback interface for a physical or virtual single firewall.
    To create a loopback interface, call from the engine node::

        engine.loopback_interface.add_single(...)
    """

    typeof = "loopback_node_dedicated_interface"

    def __init__(self, data, engine=None):
        self._engine = engine
        super(LoopbackInterface, self).__init__(data)

    @classmethod
    def create(cls, address, rank=1, nodeid=1, ospf_area=None, **kwargs):
        return super(LoopbackInterface, cls).create(
            interface_id="Loopback Interface",
            rank=rank,
            address=address,
            network_value="{}/32".format(address),
            nodeid=nodeid,
            ospfv2_area_ref=ospf_area,
            **kwargs
        )

    def add_node_loopback(self, addresses, **kwargs):
        """
        Add a node loopback interface to this engine.

        :param dict addresses: {nodeid: {'address':'127.0.0.1', 'ospf_area':ospfArea}, ...}
        :raises UpdateElementFailed: failure to create loopback address
        :return: None
        """
        engine_to_update = False

        for nodeid in addresses:
            lb_settings = addresses[nodeid]
            # retrieve the correct node
            node = None
            for n in self._engine.nodes:
                if n.data["nodeid"] == nodeid:
                    node = n
                    break
            if not (node):
                raise UpdateElementFailed(
                    "The specified nodeid {} is linked to none node".format(str(nodeid))
                )
            # compute the correct rank value
            last_rank = 1
            loopbacks = []

            if self.typeof in node.data:
                loopbacks = node.data[self.typeof]
                for loopback in loopbacks:
                    if loopback["rank"] > last_rank:
                        last_rank = loopback["rank"]
            else:
                node.data[self.typeof] = loopbacks

            ospfArea = None
            if "ospf_area" in lb_settings:
                ospfArea = lb_settings["ospf_area"]
            lb = self.create(lb_settings["address"], last_rank + 1, nodeid, ospfArea, **kwargs)
            loopbacks.append(lb.data)
            engine_to_update = True

        if engine_to_update:
            self._engine.update()

    def add_single(self, address, rank=1, nodeid=1, ospf_area=None, **kwargs):
        """
        Add a single loopback interface to this engine. This is used
        for single or virtual engines.

        :param str address: ip address for loopback
        :param int nodeid: nodeid to apply. Default to 1 for single engine
        :param str, Element ospf_area: ospf area href or element
        :raises UpdateElementFailed: failure to create loopback address
        :return: None
        """
        lb_settings = {nodeid: {"address": address, "ospf_area": ospf_area}}
        self.add_node_loopback(addresses=lb_settings, **kwargs)

    def delete(self):
        """
        Delete a loopback interface from this engine. Changes to the
        engine configuration are done immediately.

        A simple way to obtain an existing loopback is to iterate the
        loopbacks or to get by address::

            lb = engine.loopback_interface.get('127.0.0.10')
            lb.delete()

        .. warning:: When deleting a loopback assigned to a node on a cluster
            all loopbacks with the same rank will also be removed.

        :raises UpdateElementFailed: failure to delete loopback interface
        :return: None
        """
        nodes = []
        for node in self._engine.nodes:
            node.data[self.typeof] = [
                lb for lb in node.loopback_node_dedicated_interface if lb.get("rank") != self.rank
            ]
            nodes.append({node.type: node.data})

        self._engine.data["nodes"] = nodes
        self._engine.update()

    def change_ipaddress(self, address):
        self.update(address=address, network_value="{}/32".format(address))

    def __repr__(self):
        return "LoopbackInterface(address={}, nodeid={}, rank={})".format(
            self.address, self.nodeid, self.rank
        )


def inheritors(klass):
    subclasses = {}
    work = [klass]
    while work:
        parent = work.pop()
        for child in parent.__subclasses__():
            if child.typeof not in subclasses:
                subclasses[child.typeof] = child
                work.append(child)
    return subclasses


clsmembers = inheritors(SubInterface)
