"""
Runner to provide F5 Load Balancer functionality

:depends:   - pycontrol Python module

:configuration: In order to connect to a F5 Load Balancer, you must specify
    in the Salt master configuration the currently available load balancers

    .. code-block:: yaml

        load_balancers:
          bigip1.example.com:
            username: admin
            password: secret
          bigip2.example.com:
            username: admin
            password: secret
"""

from salt.exceptions import CommandExecutionError

try:
    import pycontrol.pycontrol as f5

    HAS_PYCONTROL = True
except ImportError:
    HAS_PYCONTROL = False


def __virtual__():
    if not HAS_PYCONTROL:
        return False
    return True


class F5Mgmt:
    def __init__(self, lb, username, password):
        self.lb = lb
        self.username = username
        self.password = password
        self._connect()

    def _connect(self):
        """
        Connect to F5
        """
        try:
            self.bigIP = f5.BIGIP(
                hostname=self.lb,
                username=self.username,
                password=self.password,
                fromurl=True,
                wsdls=["LocalLB.VirtualServer", "LocalLB.Pool"],
            )
        except Exception:  # pylint: disable=broad-except
            raise Exception(f"Unable to connect to {self.lb}")

        return True

    def create_vs(self, name, ip, port, protocol, profile, pool_name):
        """
        Create a virtual server
        """
        vs = self.bigIP.LocalLB.VirtualServer
        vs_def = vs.typefactory.create("Common.VirtualServerDefinition")

        vs_def.name = name
        vs_def.address = ip
        vs_def.port = port

        common_protocols = vs.typefactory.create("Common.ProtocolType")

        p = [i[0] for i in common_protocols if i[0].split("_")[1] == protocol.upper()]

        if p:
            vs_def.protocol = p
        else:
            raise CommandExecutionError("Unknown protocol")

        vs_def_seq = vs.typefactory.create("Common.VirtualServerSequence")
        vs_def_seq.item = [vs_def]

        vs_type = vs.typefactory.create("LocalLB.VirtualServer.VirtualServerType")
        vs_resource = vs.typefactory.create(
            "LocalLB.VirtualServer.VirtualServerResource"
        )

        vs_resource.type = vs_type.RESOURCE_TYPE_POOL
        vs_resource.default_pool_name = pool_name

        resource_seq = vs.typefactory.create(
            "LocalLB.VirtualServer.VirtualServerResourceSequence"
        )

        resource_seq.item = [vs_resource]

        vs_context = vs.typefactory.create("LocalLB.ProfileContextType")
        vs_profile = vs.typefactory.create("LocalLB.VirtualServer.VirtualServerProfile")

        vs_profile.profile_context = vs_context.PROFILE_CONTEXT_TYPE_ALL
        vs_profile.profile_name = protocol

        vs_profile_http = vs.typefactory.create(
            "LocalLB.VirtualServer.VirtualServerProfile"
        )
        vs_profile_http.profile_name = profile

        vs_profile_conn = vs.typefactory.create(
            "LocalLB.VirtualServer.VirtualServerProfile"
        )
        vs_profile_conn.profile_name = "oneconnect"

        vs_profile_seq = vs.typefactory.create(
            "LocalLB.VirtualServer.VirtualServerProfileSequence"
        )
        vs_profile_seq.item = [vs_profile, vs_profile_http, vs_profile_conn]

        try:
            vs.create(
                definitions=vs_def_seq,
                wildmasks=["255.255.255.255"],
                resources=resource_seq,
                profiles=[vs_profile_seq],
            )
        except Exception as e:  # pylint: disable=broad-except
            raise Exception(f"Unable to create `{name}` virtual server\n\n{e}")
        return True

    def create_pool(self, name, method="ROUND_ROBIN"):
        """
        Create a pool on the F5 load balancer
        """
        lbmethods = self.bigIP.LocalLB.Pool.typefactory.create("LocalLB.LBMethod")

        supported_method = [
            i[0] for i in lbmethods if (i[0].split("_", 2)[-1] == method.upper())
        ]

        if supported_method and not self.check_pool(name):
            try:
                self.bigIP.LocalLB.Pool.create(
                    pool_names=[name], lb_methods=[supported_method], members=[[]]
                )
            except Exception as e:  # pylint: disable=broad-except
                raise Exception(f"Unable to create `{name}` pool\n\n{e}")
        else:
            raise Exception("Unsupported method")
        return True

    def add_pool_member(self, name, port, pool_name):
        """
        Add a node to a pool
        """
        if not self.check_pool(pool_name):
            raise CommandExecutionError(f"{pool_name} pool does not exists")

        members_seq = self.bigIP.LocalLB.Pool.typefactory.create(
            "Common.IPPortDefinitionSequence"
        )
        members_seq.items = []

        member = self.bigIP.LocalLB.Pool.typefactory.create("Common.IPPortDefinition")

        member.address = name
        member.port = port

        members_seq.items.append(member)

        try:
            self.bigIP.LocalLB.Pool.add_member(
                pool_names=[pool_name], members=[members_seq]
            )
        except Exception as e:  # pylint: disable=broad-except
            raise Exception(f"Unable to add `{name}` to `{pool_name}`\n\n{e}")
        return True

    def check_pool(self, name):
        """
        Check to see if a pool exists
        """
        pools = self.bigIP.LocalLB.Pool
        for pool in pools.get_list():
            if pool.split("/")[-1] == name:
                return True
        return False

    def check_virtualserver(self, name):
        """
        Check to see if a virtual server exists
        """
        vs = self.bigIP.LocalLB.VirtualServer
        for v in vs.get_list():
            if v.split("/")[-1] == name:
                return True
        return False

    def check_member_pool(self, member, pool_name):
        """
        Check a pool member exists in a specific pool
        """
        members = self.bigIP.LocalLB.Pool.get_member(pool_names=[pool_name])[0]
        for mem in members:
            if member == mem.address:
                return True
        return False

    def lbmethods(self):
        """
        List all the load balancer methods
        """
        methods = self.bigIP.LocalLB.Pool.typefactory.create("LocalLB.LBMethod")
        return [method[0].split("_", 2)[-1] for method in methods]


def create_vs(lb, name, ip, port, protocol, profile, pool_name):
    """
    Create a virtual server

    CLI Examples:

    .. code-block:: bash

        salt-run f5.create_vs lbalancer vs_name 10.0.0.1 80 tcp http poolname
    """
    if __opts__["load_balancers"].get(lb, None):
        (username, password) = list(__opts__["load_balancers"][lb].values())
    else:
        raise Exception(f"Unable to find `{lb}` load balancer")

    F5 = F5Mgmt(lb, username, password)
    F5.create_vs(name, ip, port, protocol, profile, pool_name)
    return True


def create_pool(lb, name, method="ROUND_ROBIN"):
    """
    Create a pool on the F5 load balancer

    CLI Examples:

    .. code-block:: bash

        salt-run f5.create_pool load_balancer pool_name loadbalance_method
        salt-run f5.create_pool load_balancer my_pool ROUND_ROBIN
    """
    if __opts__["load_balancers"].get(lb, None):
        (username, password) = list(__opts__["load_balancers"][lb].values())
    else:
        raise Exception(f"Unable to find `{lb}` load balancer")
    F5 = F5Mgmt(lb, username, password)
    F5.create_pool(name, method)
    return True


def add_pool_member(lb, name, port, pool_name):
    """
    Add a node to a pool

    CLI Examples:

    .. code-block:: bash

        salt-run f5.add_pool_member load_balancer 10.0.0.1 80 my_pool
    """
    if __opts__["load_balancers"].get(lb, None):
        (username, password) = list(__opts__["load_balancers"][lb].values())
    else:
        raise Exception(f"Unable to find `{lb}` load balancer")
    F5 = F5Mgmt(lb, username, password)
    F5.add_pool_member(name, port, pool_name)
    return True


def check_pool(lb, name):
    """
    Check to see if a pool exists

    CLI Examples:

    .. code-block:: bash

        salt-run f5.check_pool load_balancer pool_name
    """
    if __opts__["load_balancers"].get(lb, None):
        (username, password) = list(__opts__["load_balancers"][lb].values())
    else:
        raise Exception(f"Unable to find `{lb}` load balancer")
    F5 = F5Mgmt(lb, username, password)
    return F5.check_pool(name)


def check_virtualserver(lb, name):
    """
    Check to see if a virtual server exists

    CLI Examples:

    .. code-block:: bash

        salt-run f5.check_virtualserver load_balancer virtual_server
    """
    if __opts__["load_balancers"].get(lb, None):
        (username, password) = list(__opts__["load_balancers"][lb].values())
    else:
        raise Exception(f"Unable to find `{lb}` load balancer")
    F5 = F5Mgmt(lb, username, password)
    return F5.check_virtualserver(name)


def check_member_pool(lb, member, pool_name):
    """
    Check a pool member exists in a specific pool

    CLI Examples:

    .. code-block:: bash

        salt-run f5.check_member_pool load_balancer 10.0.0.1 my_pool
    """
    if __opts__["load_balancers"].get(lb, None):
        (username, password) = list(__opts__["load_balancers"][lb].values())
    else:
        raise Exception(f"Unable to find `{lb}` load balancer")
    F5 = F5Mgmt(lb, username, password)
    return F5.check_member_pool(member, pool_name)
