"""
Apache Libcloud Load Balancer Management
========================================

Connection module for Apache Libcloud Storage load balancer management for a full list
of supported clouds, see http://libcloud.readthedocs.io/en/latest/loadbalancer/supported_providers.html

Clouds include Amazon ELB, ALB, Google, Aliyun, CloudStack, Softlayer

.. versionadded:: 2018.3.0

:configuration:
    This module uses a configuration profile for one or multiple Storage providers

    .. code-block:: yaml

        libcloud_loadbalancer:
            profile_test1:
              driver: gce
              key: GOOG0123456789ABCXYZ
              secret: mysecret
            profile_test2:
              driver: alb
              key: 12345
              secret: mysecret

:depends: apache-libcloud
"""
# keep lint from choking on _get_conn and _cache_id
# pylint: disable=E0602


import logging

import salt.utils.args
import salt.utils.compat
from salt.utils.versions import LooseVersion as _LooseVersion

log = logging.getLogger(__name__)

REQUIRED_LIBCLOUD_VERSION = "1.5.0"
try:
    # pylint: disable=unused-import
    import libcloud
    from libcloud.loadbalancer.providers import get_driver
    from libcloud.loadbalancer.base import Member, Algorithm

    # pylint: enable=unused-import
    if hasattr(libcloud, "__version__") and _LooseVersion(
        libcloud.__version__
    ) < _LooseVersion(REQUIRED_LIBCLOUD_VERSION):
        raise ImportError()
    logging.getLogger("libcloud").setLevel(logging.CRITICAL)
    HAS_LIBCLOUD = True
except ImportError:
    HAS_LIBCLOUD = False


def __virtual__():
    """
    Only load if libcloud libraries exist.
    """
    if not HAS_LIBCLOUD:
        return (
            False,
            "A apache-libcloud library with version at least {} was not found".format(
                REQUIRED_LIBCLOUD_VERSION
            ),
        )
    return True


def _algorithm_maps():
    return {
        "RANDOM": Algorithm.RANDOM,
        "ROUND_ROBIN": Algorithm.ROUND_ROBIN,
        "LEAST_CONNECTIONS": Algorithm.LEAST_CONNECTIONS,
        "WEIGHTED_ROUND_ROBIN": Algorithm.WEIGHTED_ROUND_ROBIN,
        "WEIGHTED_LEAST_CONNECTIONS": Algorithm.WEIGHTED_LEAST_CONNECTIONS,
        "SHORTEST_RESPONSE": Algorithm.SHORTEST_RESPONSE,
        "PERSISTENT_IP": Algorithm.PERSISTENT_IP,
    }


def _get_driver(profile):
    config = __salt__["config.option"]("libcloud_loadbalancer")[profile]
    cls = get_driver(config["driver"])
    args = config.copy()
    del args["driver"]
    args["key"] = config.get("key")
    args["secret"] = config.get("secret", None)
    if args["secret"] is None:
        del args["secret"]
    args["secure"] = config.get("secure", True)
    args["host"] = config.get("host", None)
    args["port"] = config.get("port", None)
    return cls(**args)


def list_balancers(profile, **libcloud_kwargs):
    """
    Return a list of load balancers.

    :param profile: The profile key
    :type  profile: ``str``

    :param libcloud_kwargs: Extra arguments for the driver's list_balancers method
    :type  libcloud_kwargs: ``dict``

    CLI Example:

    .. code-block:: bash

        salt myminion libcloud_storage.list_balancers profile1
    """
    conn = _get_driver(profile=profile)
    libcloud_kwargs = salt.utils.args.clean_kwargs(**libcloud_kwargs)
    balancers = conn.list_balancers(**libcloud_kwargs)
    ret = []
    for balancer in balancers:
        ret.append(_simple_balancer(balancer))
    return ret


def list_protocols(profile, **libcloud_kwargs):
    """
    Return a list of supported protocols.

    :param profile: The profile key
    :type  profile: ``str``

    :param libcloud_kwargs: Extra arguments for the driver's list_protocols method
    :type  libcloud_kwargs: ``dict``

    :return: a list of supported protocols
    :rtype: ``list`` of ``str``

    CLI Example:

    .. code-block:: bash

        salt myminion libcloud_storage.list_protocols profile1
    """
    conn = _get_driver(profile=profile)
    libcloud_kwargs = salt.utils.args.clean_kwargs(**libcloud_kwargs)
    return conn.list_protocols(**libcloud_kwargs)


def create_balancer(
    name, port, protocol, profile, algorithm=None, members=None, **libcloud_kwargs
):
    """
    Create a new load balancer instance

    :param name: Name of the new load balancer (required)
    :type  name: ``str``

    :param port: Port the load balancer should listen on, defaults to 80
    :type  port: ``str``

    :param protocol: Loadbalancer protocol, defaults to http.
    :type  protocol: ``str``

    :param algorithm: Load balancing algorithm, defaults to ROUND_ROBIN. See Algorithm type
        in Libcloud documentation for a full listing.
    :type algorithm: ``str``

    :param profile: The profile key
    :type  profile: ``str``

    :param libcloud_kwargs: Extra arguments for the driver's create_balancer method
    :type  libcloud_kwargs: ``dict``

    :return: The details of the new balancer

    CLI Example:

    .. code-block:: bash

        salt myminion libcloud_storage.create_balancer my_balancer 80 http profile1
    """
    if algorithm is None:
        algorithm = Algorithm.ROUND_ROBIN
    else:
        if isinstance(algorithm, str):
            algorithm = _algorithm_maps()[algorithm]
    starting_members = []
    if members is not None:
        if isinstance(members, list):
            for m in members:
                starting_members.append(Member(id=None, ip=m["ip"], port=m["port"]))
        else:
            raise ValueError("members must be of type list")

    libcloud_kwargs = salt.utils.args.clean_kwargs(**libcloud_kwargs)
    conn = _get_driver(profile=profile)
    balancer = conn.create_balancer(
        name, port, protocol, algorithm, starting_members, **libcloud_kwargs
    )
    return _simple_balancer(balancer)


def destroy_balancer(balancer_id, profile, **libcloud_kwargs):
    """
    Destroy a load balancer

    :param balancer_id: LoadBalancer ID which should be used
    :type  balancer_id: ``str``

    :param profile: The profile key
    :type  profile: ``str``

    :param libcloud_kwargs: Extra arguments for the driver's destroy_balancer method
    :type  libcloud_kwargs: ``dict``

    :return: ``True`` if the destroy was successful, otherwise ``False``.
    :rtype: ``bool``

    CLI Example:

    .. code-block:: bash

        salt myminion libcloud_storage.destroy_balancer balancer_1 profile1
    """
    conn = _get_driver(profile=profile)
    libcloud_kwargs = salt.utils.args.clean_kwargs(**libcloud_kwargs)
    balancer = conn.get_balancer(balancer_id)
    return conn.destroy_balancer(balancer, **libcloud_kwargs)


def get_balancer_by_name(name, profile, **libcloud_kwargs):
    """
    Get the details for a load balancer by name

    :param name: Name of a load balancer you want to fetch
    :type  name: ``str``

    :param profile: The profile key
    :type  profile: ``str``

    :param libcloud_kwargs: Extra arguments for the driver's list_balancers method
    :type  libcloud_kwargs: ``dict``

    :return: the load balancer details

    CLI Example:

    .. code-block:: bash

        salt myminion libcloud_storage.get_balancer_by_name my_balancer profile1
    """
    conn = _get_driver(profile=profile)
    libcloud_kwargs = salt.utils.args.clean_kwargs(**libcloud_kwargs)
    balancers = conn.list_balancers(**libcloud_kwargs)
    match = [b for b in balancers if b.name == name]
    if len(match) == 1:
        return _simple_balancer(match[0])
    elif len(match) > 1:
        raise ValueError("Ambiguous argument, found mulitple records")
    else:
        raise ValueError("Bad argument, found no records")


def get_balancer(balancer_id, profile, **libcloud_kwargs):
    """
    Get the details for a load balancer by ID

    :param balancer_id: id of a load balancer you want to fetch
    :type  balancer_id: ``str``

    :param profile: The profile key
    :type  profile: ``str``

    :param libcloud_kwargs: Extra arguments for the driver's get_balancer method
    :type  libcloud_kwargs: ``dict``

    :return: the load balancer details

    CLI Example:

    .. code-block:: bash

        salt myminion libcloud_storage.get_balancer balancer123 profile1
    """
    conn = _get_driver(profile=profile)
    libcloud_kwargs = salt.utils.args.clean_kwargs(**libcloud_kwargs)
    balancer = conn.get_balancer(balancer_id, **libcloud_kwargs)
    return _simple_balancer(balancer)


def list_supported_algorithms(profile, **libcloud_kwargs):
    """
    Get the supported algorithms for a profile

    :param profile: The profile key
    :type  profile: ``str``

    :param libcloud_kwargs: Extra arguments for the driver's list_supported_algorithms method
    :type  libcloud_kwargs: ``dict``

    :return: The supported algorithms

    CLI Example:

    .. code-block:: bash

        salt myminion libcloud_storage.list_supported_algorithms profile1
    """
    conn = _get_driver(profile=profile)
    libcloud_kwargs = salt.utils.args.clean_kwargs(**libcloud_kwargs)
    return conn.list_supported_algorithms(**libcloud_kwargs)


def balancer_attach_member(
    balancer_id, ip, port, profile, extra=None, **libcloud_kwargs
):
    """
    Add a new member to the load balancer

    :param balancer_id: id of a load balancer you want to fetch
    :type  balancer_id: ``str``

    :param ip: IP address for the new member
    :type  ip: ``str``

    :param port: Port for the new member
    :type  port: ``int``

    :param profile: The profile key
    :type  profile: ``str``

    :param libcloud_kwargs: Extra arguments for the driver's balancer_attach_member method
    :type  libcloud_kwargs: ``dict``

    CLI Example:

    .. code-block:: bash

        salt myminion libcloud_storage.balancer_attach_member balancer123 1.2.3.4 80 profile1
    """
    conn = _get_driver(profile=profile)
    libcloud_kwargs = salt.utils.args.clean_kwargs(**libcloud_kwargs)
    member = Member(id=None, ip=ip, port=port, balancer=None, extra=extra)
    balancer = conn.get_balancer(balancer_id)
    member_saved = conn.balancer_attach_member(balancer, member, **libcloud_kwargs)
    return _simple_member(member_saved)


def balancer_detach_member(balancer_id, member_id, profile, **libcloud_kwargs):
    """
    Add a new member to the load balancer

    :param balancer_id: id of a load balancer you want to fetch
    :type  balancer_id: ``str``

    :param ip: IP address for the new member
    :type  ip: ``str``

    :param port: Port for the new member
    :type  port: ``int``

    :param profile: The profile key
    :type  profile: ``str``

    :param libcloud_kwargs: Extra arguments for the driver's balancer_detach_member method
    :type  libcloud_kwargs: ``dict``

    CLI Example:

    .. code-block:: bash

        salt myminion libcloud_storage.balancer_detach_member balancer123 member123 profile1
    """
    conn = _get_driver(profile=profile)
    balancer = conn.get_balancer(balancer_id)
    members = conn.balancer_list_members(balancer=balancer)
    match = [member for member in members if member.id == member_id]
    if len(match) > 1:
        raise ValueError("Ambiguous argument, found mulitple records")
    elif not match:
        raise ValueError("Bad argument, found no records")
    else:
        member = match[0]
    libcloud_kwargs = salt.utils.args.clean_kwargs(**libcloud_kwargs)
    return conn.balancer_detach_member(
        balancer=balancer, member=member, **libcloud_kwargs
    )


def list_balancer_members(balancer_id, profile, **libcloud_kwargs):
    """
    List the members of a load balancer

    :param balancer_id: id of a load balancer you want to fetch
    :type  balancer_id: ``str``

    :param profile: The profile key
    :type  profile: ``str``

    :param libcloud_kwargs: Extra arguments for the driver's list_balancer_members method
    :type  libcloud_kwargs: ``dict``

    CLI Example:

    .. code-block:: bash

        salt myminion libcloud_storage.list_balancer_members balancer123 profile1
    """
    conn = _get_driver(profile=profile)
    balancer = conn.get_balancer(balancer_id)
    libcloud_kwargs = salt.utils.args.clean_kwargs(**libcloud_kwargs)
    members = conn.balancer_list_members(balancer=balancer, **libcloud_kwargs)
    return [_simple_member(member) for member in members]


def extra(method, profile, **libcloud_kwargs):
    """
    Call an extended method on the driver

    :param method: Driver's method name
    :type  method: ``str``

    :param profile: The profile key
    :type  profile: ``str``

    :param libcloud_kwargs: Extra arguments for the driver's method
    :type  libcloud_kwargs: ``dict``

    CLI Example:

    .. code-block:: bash

        salt myminion libcloud_loadbalancer.extra ex_get_permissions google container_name=my_container object_name=me.jpg --out=yaml
    """
    libcloud_kwargs = salt.utils.args.clean_kwargs(**libcloud_kwargs)
    conn = _get_driver(profile=profile)
    connection_method = getattr(conn, method)
    return connection_method(**libcloud_kwargs)


def _simple_balancer(balancer):
    return {
        "id": balancer.id,
        "name": balancer.name,
        "state": balancer.state,
        "ip": balancer.ip,
        "port": balancer.port,
        "extra": balancer.extra,
    }


def _simple_member(member):
    return {
        "id": member.id,
        "ip": member.ip,
        "port": member.port,
        "balancer": _simple_balancer(member.balancer),
        "extra": member.extra,
    }
