"""
Vagrant Cloud Driver
====================

The Vagrant cloud is designed to "vagrant up" a virtual machine as a
Salt minion.

Use of this module requires some configuration in cloud profile and provider
files as described in the
:ref:`Getting Started with Vagrant <getting-started-with-vagrant>` documentation.

.. versionadded:: 2018.3.0


"""

import logging
import os
import tempfile

import salt.client
import salt.config as config
import salt.utils.cloud
from salt._compat import ipaddress
from salt.exceptions import SaltCloudException, SaltCloudSystemExit, SaltInvocationError

log = logging.getLogger(__name__)


def __virtual__():
    """
    Needs no special configuration
    """
    return True


def _get_active_provider_name():
    try:
        return __active_provider_name__.value()
    except AttributeError:
        return __active_provider_name__


def avail_locations(call=None):
    r"""
    This function returns a list of locations available.

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-locations my-cloud-provider

        # \[ vagrant will always returns an empty dictionary \]

    """

    return {}


def avail_images(call=None):
    """This function returns a list of images available for this cloud provider.
    vagrant will return a list of profiles.
    salt-cloud --list-images my-cloud-provider
    """
    vm_ = get_configured_provider()
    return {"Profiles": [profile for profile in vm_["profiles"]]}


def avail_sizes(call=None):
    r"""
    This function returns a list of sizes available for this cloud provider.

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-sizes my-cloud-provider

        # \[ vagrant always returns an empty dictionary \]

    """
    return {}


def list_nodes(call=None):
    """
    List the nodes which have salt-cloud:driver:vagrant grains.

    CLI Example:

    .. code-block:: bash

        salt-cloud -Q
    """
    nodes = _list_nodes(call)
    return _build_required_items(nodes)


def _build_required_items(nodes):
    ret = {}
    for name, grains in nodes.items():
        if grains:
            private_ips = []
            public_ips = []
            ips = grains["ipv4"] + grains["ipv6"]
            for adrs in ips:
                ip_ = ipaddress.ip_address(adrs)
                if not ip_.is_loopback:
                    if ip_.is_private:
                        private_ips.append(adrs)
                    else:
                        public_ips.append(adrs)

            ret[name] = {
                "id": grains["id"],
                "image": grains["salt-cloud"]["profile"],
                "private_ips": private_ips,
                "public_ips": public_ips,
                "size": "",
                "state": "running",
            }

    return ret


def list_nodes_full(call=None):
    """
    List the nodes, ask all 'vagrant' minions, return dict of grains (enhanced).

    CLI Example:

    .. code-block:: bash

        salt-call -F
    """
    ret = _list_nodes(call)

    for (
        key,
        grains,
    ) in ret.items():  # clean up some hyperverbose grains -- everything is too much
        try:
            del (
                grains["cpu_flags"],
                grains["disks"],
                grains["pythonpath"],
                grains["dns"],
                grains["gpus"],
            )
        except KeyError:
            pass  # ignore absence of things we are eliminating
        except TypeError:
            del ret[key]  # eliminate all reference to unexpected (None) values.

    reqs = _build_required_items(ret)
    for name in ret:
        ret[name].update(reqs[name])
    return ret


def _list_nodes(call=None):
    """
    List the nodes, ask all 'vagrant' minions, return dict of grains.
    """
    with salt.client.LocalClient() as local:
        return local.cmd(
            "salt-cloud:driver:vagrant", "grains.items", "", tgt_type="grain"
        )


def list_nodes_select(call=None):
    """
    Return a list of the minions that have salt-cloud grains, with
    select fields.
    """
    return salt.utils.cloud.list_nodes_select(
        list_nodes_full("function"),
        __opts__["query.selection"],
        call,
    )


def show_instance(name, call=None):
    """
    List the a single node, return dict of grains.
    """
    with salt.client.LocalClient() as local:
        ret = local.cmd(name, "grains.items", "")
        reqs = _build_required_items(ret)
        ret[name].update(reqs[name])
        return ret


def _get_my_info(name):
    with salt.client.LocalClient() as local:
        return local.cmd(name, "grains.get", ["salt-cloud"])


def create(vm_):
    """
    Provision a single machine

    CLI Example:

    .. code-block:: bash

        salt-cloud -p my_profile new_node_1

    """
    name = vm_["name"]
    machine = config.get_cloud_config_value("machine", vm_, __opts__, default="")
    vm_["machine"] = machine
    host = config.get_cloud_config_value("host", vm_, __opts__, default=NotImplemented)
    vm_["cwd"] = config.get_cloud_config_value("cwd", vm_, __opts__, default="/")
    vm_["runas"] = config.get_cloud_config_value(
        "vagrant_runas", vm_, __opts__, default=os.getenv("SUDO_USER")
    )
    vm_["timeout"] = config.get_cloud_config_value(
        "vagrant_up_timeout", vm_, __opts__, default=300
    )
    vm_["vagrant_provider"] = config.get_cloud_config_value(
        "vagrant_provider", vm_, __opts__, default=""
    )
    vm_["grains"] = {"salt-cloud:vagrant": {"host": host, "machine": machine}}

    log.info("sending 'vagrant.init %s machine=%s' command to %s", name, machine, host)

    with salt.client.LocalClient() as local:
        ret = local.cmd(host, "vagrant.init", [name], kwarg={"vm": vm_, "start": True})
        log.info("response ==> %s", ret[host])

        network_mask = config.get_cloud_config_value(
            "network_mask", vm_, __opts__, default=""
        )
        if "ssh_host" not in vm_:
            ret = local.cmd(
                host,
                "vagrant.get_ssh_config",
                [name],
                kwarg={"network_mask": network_mask, "get_private_key": True},
            )[host]
    with tempfile.NamedTemporaryFile() as pks:
        if "private_key" not in vm_ and ret and ret.get("private_key", False):
            pks.write(ret["private_key"])
            pks.flush()
            log.debug("wrote private key to %s", pks.name)
            vm_["key_filename"] = pks.name
        if "ssh_host" not in vm_:
            try:
                vm_.setdefault("ssh_username", ret["ssh_username"])
                if ret.get("ip_address"):
                    vm_["ssh_host"] = ret["ip_address"]
                else:  # if probe failed or not used, use Vagrant's reported ssh info
                    vm_["ssh_host"] = ret["ssh_host"]
                    vm_.setdefault("ssh_port", ret["ssh_port"])
            except (KeyError, TypeError):
                raise SaltInvocationError(
                    "Insufficient SSH addressing information for {}".format(name)
                )

        log.info(
            "Provisioning machine %s as node %s using ssh %s",
            machine,
            name,
            vm_["ssh_host"],
        )
        ret = __utils__["cloud.bootstrap"](vm_, __opts__)
        return ret


def get_configured_provider():
    """
    Return the first configured instance.
    """
    ret = config.is_provider_configured(
        __opts__, _get_active_provider_name() or "vagrant", ""
    )
    return ret


# noinspection PyTypeChecker
def destroy(name, call=None):
    """
    Destroy a node.

    CLI Example:

    .. code-block:: bash

        salt-cloud --destroy mymachine
    """
    if call == "function":
        raise SaltCloudSystemExit(
            "The destroy action must be called with -d, --destroy, -a, or --action."
        )

    opts = __opts__

    __utils__["cloud.fire_event"](
        "event",
        "destroying instance",
        "salt/cloud/{}/destroying".format(name),
        args={"name": name},
        sock_dir=opts["sock_dir"],
        transport=opts["transport"],
    )
    my_info = _get_my_info(name)
    if my_info:
        profile_name = my_info[name]["profile"]
        profile = opts["profiles"][profile_name]
        host = profile["host"]
        with salt.client.LocalClient() as local:
            ret = local.cmd(host, "vagrant.destroy", [name])

        if ret[host]:
            __utils__["cloud.fire_event"](
                "event",
                "destroyed instance",
                "salt/cloud/{}/destroyed".format(name),
                args={"name": name},
                sock_dir=opts["sock_dir"],
                transport=opts["transport"],
            )

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

            return {"Destroyed": "{} was destroyed.".format(name)}
        else:
            return {"Error": "Error destroying {}".format(name)}
    else:
        return {"Error": "No response from {}. Cannot destroy.".format(name)}


# noinspection PyTypeChecker
def reboot(name, call=None):
    """
    Reboot a vagrant minion.

    name
        The name of the VM to reboot.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a reboot vm_name
    """
    if call != "action":
        raise SaltCloudException(
            "The reboot action must be called with -a or --action."
        )
    my_info = _get_my_info(name)
    profile_name = my_info[name]["profile"]
    profile = __opts__["profiles"][profile_name]
    host = profile["host"]
    with salt.client.LocalClient() as local:
        return local.cmd(host, "vagrant.reboot", [name])
