# -*- coding: utf-8 -*-
"""
The generic libcloud template used to create the connections and deploy the
cloud virtual machines
"""
from __future__ import absolute_import, print_function, unicode_literals

import logging

# Import python libs
import os

import salt.client
import salt.config as config

# Import salt cloud libs
import salt.utils.cloud
import salt.utils.data

# Import salt libs
import salt.utils.event
from salt.exceptions import SaltCloudNotFound, SaltCloudSystemExit
from salt.ext import six
from salt.ext.six.moves import zip

# pylint: disable=W0611
# Import libcloud
try:
    import libcloud
    import re
    from libcloud.compute.types import Provider
    from libcloud.compute.providers import get_driver
    from libcloud.compute.deployment import MultiStepDeployment, ScriptDeployment

    HAS_LIBCLOUD = True
    LIBCLOUD_VERSION_INFO = tuple(
        [
            int(part)
            for part in libcloud.__version__.replace("-", ".")
            .replace("rc", ".")
            .split(".")[:3]
        ]
    )

except ImportError:
    HAS_LIBCLOUD = False
    LIBCLOUD_VERSION_INFO = (1000,)
# pylint: enable=W0611


# Get logging started
log = logging.getLogger(__name__)


LIBCLOUD_MINIMAL_VERSION = (0, 14, 0)


def node_state(id_):
    """
    Libcloud supported node states
    """
    states_int = {
        0: "RUNNING",
        1: "REBOOTING",
        2: "TERMINATED",
        3: "PENDING",
        4: "UNKNOWN",
        5: "STOPPED",
        6: "SUSPENDED",
        7: "ERROR",
        8: "PAUSED",
    }
    states_str = {
        "running": "RUNNING",
        "rebooting": "REBOOTING",
        "starting": "STARTING",
        "terminated": "TERMINATED",
        "pending": "PENDING",
        "unknown": "UNKNOWN",
        "stopping": "STOPPING",
        "stopped": "STOPPED",
        "suspended": "SUSPENDED",
        "error": "ERROR",
        "paused": "PAUSED",
        "reconfiguring": "RECONFIGURING",
    }
    return states_str[id_] if isinstance(id_, six.string_types) else states_int[id_]


def check_libcloud_version(reqver=LIBCLOUD_MINIMAL_VERSION, why=None):
    """
    Compare different libcloud versions
    """
    if not HAS_LIBCLOUD:
        return False

    if not isinstance(reqver, (list, tuple)):
        raise RuntimeError(
            "'reqver' needs to passed as a tuple or list, i.e., (0, 14, 0)"
        )
    try:
        import libcloud  # pylint: disable=redefined-outer-name
    except ImportError:
        raise ImportError(
            "salt-cloud requires >= libcloud {0} which is not installed".format(
                ".".join([six.text_type(num) for num in reqver])
            )
        )

    if LIBCLOUD_VERSION_INFO >= reqver:
        return libcloud.__version__

    errormsg = "Your version of libcloud is {0}. ".format(libcloud.__version__)
    errormsg += "salt-cloud requires >= libcloud {0}".format(
        ".".join([six.text_type(num) for num in reqver])
    )
    if why:
        errormsg += " for {0}".format(why)
    errormsg += ". Please upgrade."
    raise ImportError(errormsg)


def get_node(conn, name):
    """
    Return a libcloud node for the named VM
    """
    nodes = conn.list_nodes()
    for node in nodes:
        if node.name == name:
            __utils__["cloud.cache_node"](
                salt.utils.data.simple_types_filter(node.__dict__),
                __active_provider_name__,
                __opts__,
            )
            return node


def avail_locations(conn=None, call=None):
    """
    Return a dict of all available VM locations on the cloud provider with
    relevant data
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_locations function must be called with "
            "-f or --function, or with the --list-locations option"
        )

    if not conn:
        conn = get_conn()  # pylint: disable=E0602

    locations = conn.list_locations()
    ret = {}
    for img in locations:
        if isinstance(img.name, six.string_types) and not six.PY3:
            img_name = img.name.encode("ascii", "salt-cloud-force-ascii")
        else:
            img_name = str(img.name)  # future lint: disable=blacklisted-function

        ret[img_name] = {}
        for attr in dir(img):
            if attr.startswith("_") or attr == "driver":
                continue

            attr_value = getattr(img, attr)
            if isinstance(attr_value, six.string_types) and not six.PY3:
                attr_value = attr_value.encode("ascii", "salt-cloud-force-ascii")
            ret[img_name][attr] = attr_value

    return ret


def avail_images(conn=None, call=None):
    """
    Return a dict of all available VM images on the cloud provider with
    relevant data
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_images function must be called with "
            "-f or --function, or with the --list-images option"
        )

    if not conn:
        conn = get_conn()  # pylint: disable=E0602

    images = conn.list_images()
    ret = {}
    for img in images:
        if isinstance(img.name, six.string_types) and not six.PY3:
            img_name = img.name.encode("ascii", "salt-cloud-force-ascii")
        else:
            img_name = str(img.name)  # future lint: disable=blacklisted-function

        ret[img_name] = {}
        for attr in dir(img):
            if attr.startswith("_") or attr in ("driver", "get_uuid"):
                continue
            attr_value = getattr(img, attr)
            if isinstance(attr_value, six.string_types) and not six.PY3:
                attr_value = attr_value.encode("ascii", "salt-cloud-force-ascii")
            ret[img_name][attr] = attr_value
    return ret


def avail_sizes(conn=None, call=None):
    """
    Return a dict of all available VM images on the cloud provider with
    relevant data
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_sizes function must be called with "
            "-f or --function, or with the --list-sizes option"
        )

    if not conn:
        conn = get_conn()  # pylint: disable=E0602

    sizes = conn.list_sizes()
    ret = {}
    for size in sizes:
        if isinstance(size.name, six.string_types) and not six.PY3:
            size_name = size.name.encode("ascii", "salt-cloud-force-ascii")
        else:
            size_name = str(size.name)  # future lint: disable=blacklisted-function

        ret[size_name] = {}
        for attr in dir(size):
            if attr.startswith("_") or attr in ("driver", "get_uuid"):
                continue

            try:
                attr_value = getattr(size, attr)
            except Exception:  # pylint: disable=broad-except
                pass

            if isinstance(attr_value, six.string_types) and not six.PY3:
                attr_value = attr_value.encode("ascii", "salt-cloud-force-ascii")
            ret[size_name][attr] = attr_value
    return ret


def get_location(conn, vm_):
    """
    Return the location object to use
    """
    locations = conn.list_locations()
    vm_location = config.get_cloud_config_value("location", vm_, __opts__)
    if not six.PY3:
        vm_location = vm_location.encode("ascii", "salt-cloud-force-ascii")

    for img in locations:
        if isinstance(img.id, six.string_types) and not six.PY3:
            img_id = img.id.encode("ascii", "salt-cloud-force-ascii")
        else:
            img_id = str(img.id)  # future lint: disable=blacklisted-function

        if isinstance(img.name, six.string_types) and not six.PY3:
            img_name = img.name.encode("ascii", "salt-cloud-force-ascii")
        else:
            img_name = str(img.name)  # future lint: disable=blacklisted-function

        if vm_location and vm_location in (img_id, img_name):
            return img

    raise SaltCloudNotFound(
        "The specified location, '{0}', could not be found.".format(vm_location)
    )


def get_image(conn, vm_):
    """
    Return the image object to use
    """
    images = conn.list_images()
    vm_image = config.get_cloud_config_value("image", vm_, __opts__)

    if not six.PY3:
        vm_image = vm_image.encode("ascii", "salt-cloud-force-ascii")

    for img in images:
        if isinstance(img.id, six.string_types) and not six.PY3:
            img_id = img.id.encode("ascii", "salt-cloud-force-ascii")
        else:
            img_id = str(img.id)  # future lint: disable=blacklisted-function

        if isinstance(img.name, six.string_types) and not six.PY3:
            img_name = img.name.encode("ascii", "salt-cloud-force-ascii")
        else:
            img_name = str(img.name)  # future lint: disable=blacklisted-function

        if vm_image and vm_image in (img_id, img_name):
            return img

    raise SaltCloudNotFound(
        "The specified image, '{0}', could not be found.".format(vm_image)
    )


def get_size(conn, vm_):
    """
    Return the VM's size object
    """
    sizes = conn.list_sizes()
    vm_size = config.get_cloud_config_value("size", vm_, __opts__)
    if not vm_size:
        return sizes[0]

    for size in sizes:
        if vm_size and str(vm_size) in (
            str(size.id),
            str(size.name),
        ):  # pylint: disable=blacklisted-function
            return size
    raise SaltCloudNotFound(
        "The specified size, '{0}', could not be found.".format(vm_size)
    )


def script(vm_):
    """
    Return the script deployment object
    """
    return ScriptDeployment(
        salt.utils.cloud.os_script(
            config.get_cloud_config_value("os", vm_, __opts__),
            vm_,
            __opts__,
            salt.utils.cloud.salt_config_to_yaml(
                salt.utils.cloud.minion_config(__opts__, vm_)
            ),
        )
    )


def destroy(name, conn=None, call=None):
    """
    Delete a single VM
    """
    if call == "function":
        raise SaltCloudSystemExit(
            "The destroy action must be called with -d, --destroy, " "-a or --action."
        )

    __utils__["cloud.fire_event"](
        "event",
        "destroying instance",
        "salt/cloud/{0}/destroying".format(name),
        args={"name": name},
        sock_dir=__opts__["sock_dir"],
        transport=__opts__["transport"],
    )

    if not conn:
        conn = get_conn()  # pylint: disable=E0602

    node = get_node(conn, name)
    profiles = get_configured_provider()["profiles"]  # pylint: disable=E0602
    if node is None:
        log.error("Unable to find the VM %s", name)
    profile = None
    if "metadata" in node.extra and "profile" in node.extra["metadata"]:
        profile = node.extra["metadata"]["profile"]

    flush_mine_on_destroy = False
    if profile and profile in profiles and "flush_mine_on_destroy" in profiles[profile]:
        flush_mine_on_destroy = profiles[profile]["flush_mine_on_destroy"]

    if flush_mine_on_destroy:
        log.info("Clearing Salt Mine: %s", name)

        mopts_ = salt.config.DEFAULT_MINION_OPTS
        conf_path = "/".join(__opts__["conf_file"].split("/")[:-1])
        mopts_.update(salt.config.minion_config(os.path.join(conf_path, "minion")))
        client = salt.client.get_local_client(mopts_)
        minions = client.cmd(name, "mine.flush")

    log.info("Clearing Salt Mine: %s, %s", name, flush_mine_on_destroy)
    log.info("Destroying VM: %s", name)
    ret = conn.destroy_node(node)
    if ret:
        log.info("Destroyed VM: %s", name)
        # Fire destroy action
        __utils__["cloud.fire_event"](
            "event",
            "destroyed instance",
            "salt/cloud/{0}/destroyed".format(name),
            args={"name": name},
            sock_dir=__opts__["sock_dir"],
            transport=__opts__["transport"],
        )
        if __opts__["delete_sshkeys"] is True:
            public_ips = getattr(node, __opts__.get("ssh_interface", "public_ips"))
            if public_ips:
                salt.utils.cloud.remove_sshkey(public_ips[0])

            private_ips = getattr(node, __opts__.get("ssh_interface", "private_ips"))
            if private_ips:
                salt.utils.cloud.remove_sshkey(private_ips[0])

        if __opts__.get("update_cachedir", False) is True:
            __utils__["cloud.delete_minion_cachedir"](
                name, __active_provider_name__.split(":")[0], __opts__
            )

        return True

    log.error("Failed to Destroy VM: %s", name)
    return False


def reboot(name, conn=None):
    """
    Reboot a single VM
    """
    if not conn:
        conn = get_conn()  # pylint: disable=E0602

    node = get_node(conn, name)
    if node is None:
        log.error("Unable to find the VM %s", name)
    log.info("Rebooting VM: %s", name)
    ret = conn.reboot_node(node)
    if ret:
        log.info("Rebooted VM: %s", name)
        # Fire reboot action
        __utils__["cloud.fire_event"](
            "event",
            "{0} has been rebooted".format(name),
            "salt-cloud" "salt/cloud/{0}/rebooting".format(name),
            args={"name": name},
            sock_dir=__opts__["sock_dir"],
            transport=__opts__["transport"],
        )
        return True

    log.error("Failed to reboot VM: %s", name)
    return False


def list_nodes(conn=None, call=None):
    """
    Return a list of the VMs that are on the provider
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The list_nodes function must be called with -f or --function."
        )

    if not conn:
        conn = get_conn()  # pylint: disable=E0602

    nodes = conn.list_nodes()
    ret = {}
    for node in nodes:
        ret[node.name] = {
            "id": node.id,
            "image": node.image,
            "name": node.name,
            "private_ips": node.private_ips,
            "public_ips": node.public_ips,
            "size": node.size,
            "state": node_state(node.state),
        }
    return ret


def list_nodes_full(conn=None, call=None):
    """
    Return a list of the VMs that are on the provider, with all fields
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The list_nodes_full function must be called with -f or --function."
        )

    if not conn:
        conn = get_conn()  # pylint: disable=E0602

    nodes = conn.list_nodes()
    ret = {}
    for node in nodes:
        pairs = {}
        for key, value in zip(node.__dict__, six.itervalues(node.__dict__)):
            pairs[key] = value
        ret[node.name] = pairs
        del ret[node.name]["driver"]

    __utils__["cloud.cache_node_list"](
        ret, __active_provider_name__.split(":")[0], __opts__
    )
    return ret


def list_nodes_select(conn=None, call=None):
    """
    Return a list of the VMs that are on the provider, with select fields
    """
    if not conn:
        conn = get_conn()  # pylint: disable=E0602

    return salt.utils.cloud.list_nodes_select(
        list_nodes_full(conn, "function"), __opts__["query.selection"], call,
    )


def show_instance(name, call=None):
    """
    Show the details from the provider concerning an instance
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The show_instance action must be called with -a or --action."
        )

    nodes = list_nodes_full()
    __utils__["cloud.cache_node"](nodes[name], __active_provider_name__, __opts__)
    return nodes[name]


def conn_has_method(conn, method_name):
    """
    Find if the provided connection object has a specific method
    """
    if method_name in dir(conn):
        return True

    log.error("Method '%s' not yet supported!", method_name)
    return False
