"""
DigitalOcean Cloud Module
=========================

The DigitalOcean cloud module is used to control access to the DigitalOcean VPS system.

Use of this module requires a requires a ``personal_access_token``, an ``ssh_key_file``,
and at least one SSH key name in ``ssh_key_names``. More ``ssh_key_names`` can be added
by separating each key with a comma. The ``personal_access_token`` can be found in the
DigitalOcean web interface in the "Apps & API" section. The SSH key name can be found
under the "SSH Keys" section.

.. code-block:: yaml

    # Note: This example is for /etc/salt/cloud.providers or any file in the
    # /etc/salt/cloud.providers.d/ directory.

    my-digital-ocean-config:
      personal_access_token: xxx
      ssh_key_file: /path/to/ssh/key/file
      ssh_key_names: my-key-name,my-key-name-2
      driver: digitalocean

:depends: requests
"""

import decimal
import logging
import os
import pprint
import time

import salt.config as config
import salt.utils.cloud
import salt.utils.files
import salt.utils.json
import salt.utils.stringutils
from salt.exceptions import (
    SaltCloudConfigError,
    SaltCloudExecutionFailure,
    SaltCloudExecutionTimeout,
    SaltCloudNotFound,
    SaltCloudSystemExit,
    SaltInvocationError,
)

try:
    import requests

    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False

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

__virtualname__ = "digitalocean"
__virtual_aliases__ = ("digital_ocean", "do")


# Only load in this module if the DIGITALOCEAN configurations are in place
def __virtual__():
    """
    Check for DigitalOcean configurations
    """
    if get_configured_provider() is False:
        return False

    if get_dependencies() is False:
        return False

    return __virtualname__


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


def get_configured_provider():
    """
    Return the first configured instance.
    """
    return config.is_provider_configured(
        opts=__opts__,
        provider=_get_active_provider_name() or __virtualname__,
        aliases=__virtual_aliases__,
        required_keys=("personal_access_token",),
    )


def get_dependencies():
    """
    Warn if dependencies aren't met.
    """
    return config.check_driver_dependencies(__virtualname__, {"requests": HAS_REQUESTS})


def avail_locations(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"
        )

    items = query(method="regions")
    ret = {}
    for region in items["regions"]:
        ret[region["name"]] = {}
        for item in region.keys():
            ret[region["name"]][item] = str(region[item])

    return ret


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

    fetch = True
    page = 1
    ret = {}

    while fetch:
        items = query(method="images", command="?page=" + str(page) + "&per_page=200")

        for image in items["images"]:
            ret[image["name"]] = {}
            for item in image.keys():
                ret[image["name"]][item] = image[item]

        page += 1
        try:
            fetch = "next" in items["links"]["pages"]
        except KeyError:
            fetch = False

    return ret


def avail_sizes(call=None):
    """
    Return a list of the image sizes that are on the provider
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_sizes function must be called with "
            "-f or --function, or with the --list-sizes option"
        )

    items = query(method="sizes", command="?per_page=100")
    ret = {}
    for size in items["sizes"]:
        ret[size["slug"]] = {}
        for item in size.keys():
            ret[size["slug"]][item] = str(size[item])

    return ret


def list_nodes(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."
        )
    return _list_nodes()


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


def list_nodes_select(call=None):
    """
    Return a list of the VMs that are on the provider, with select fields
    """
    return salt.utils.cloud.list_nodes_select(
        list_nodes_full("function"),
        __opts__["query.selection"],
        call,
    )


def get_image(vm_):
    """
    Return the image object to use
    """
    images = avail_images()
    vm_image = config.get_cloud_config_value(
        "image", vm_, __opts__, search_global=False
    )
    if not isinstance(vm_image, str):
        vm_image = str(vm_image)

    for image in images:
        if vm_image in (
            images[image]["name"],
            images[image]["slug"],
            images[image]["id"],
        ):
            if images[image]["slug"] is not None:
                return images[image]["slug"]
            return int(images[image]["id"])
    raise SaltCloudNotFound(f"The specified image, '{vm_image}', could not be found.")


def get_size(vm_):
    """
    Return the VM's size. Used by create_node().
    """
    sizes = avail_sizes()
    vm_size = str(
        config.get_cloud_config_value("size", vm_, __opts__, search_global=False)
    )
    for size in sizes:
        if vm_size.lower() == sizes[size]["slug"]:
            return sizes[size]["slug"]
    raise SaltCloudNotFound(f"The specified size, '{vm_size}', could not be found.")


def get_location(vm_):
    """
    Return the VM's location
    """
    locations = avail_locations()
    vm_location = str(
        config.get_cloud_config_value("location", vm_, __opts__, search_global=False)
    )

    for location in locations:
        if vm_location in (locations[location]["name"], locations[location]["slug"]):
            return locations[location]["slug"]
    raise SaltCloudNotFound(
        f"The specified location, '{vm_location}', could not be found."
    )


def create_node(args):
    """
    Create a node
    """
    node = query(method="droplets", args=args, http_method="post")
    return node


def create(vm_):
    """
    Create a single VM from a data dict
    """
    try:
        # Check for required profile parameters before sending any API calls.
        if (
            vm_["profile"]
            and config.is_profile_configured(
                __opts__,
                _get_active_provider_name() or "digitalocean",
                vm_["profile"],
                vm_=vm_,
            )
            is False
        ):
            return False
    except AttributeError:
        pass

    __utils__["cloud.fire_event"](
        "event",
        "starting create",
        "salt/cloud/{}/creating".format(vm_["name"]),
        args=__utils__["cloud.filter_event"](
            "creating", vm_, ["name", "profile", "provider", "driver"]
        ),
        sock_dir=__opts__["sock_dir"],
        transport=__opts__["transport"],
    )

    log.info("Creating Cloud VM %s", vm_["name"])

    kwargs = {
        "name": vm_["name"],
        "size": get_size(vm_),
        "image": get_image(vm_),
        "region": get_location(vm_),
        "ssh_keys": [],
        "tags": [],
    }

    # backwards compat
    ssh_key_name = config.get_cloud_config_value(
        "ssh_key_name", vm_, __opts__, search_global=False
    )

    if ssh_key_name:
        kwargs["ssh_keys"].append(get_keyid(ssh_key_name))

    ssh_key_names = config.get_cloud_config_value(
        "ssh_key_names", vm_, __opts__, search_global=False, default=False
    )

    if ssh_key_names:
        for key in ssh_key_names.split(","):
            kwargs["ssh_keys"].append(get_keyid(key))

    key_filename = config.get_cloud_config_value(
        "ssh_key_file", vm_, __opts__, search_global=False, default=None
    )

    if key_filename is not None and not os.path.isfile(key_filename):
        raise SaltCloudConfigError(
            f"The defined key_filename '{key_filename}' does not exist"
        )

    if not __opts__.get("ssh_agent", False) and key_filename is None:
        raise SaltCloudConfigError(
            "The DigitalOcean driver requires an ssh_key_file and an ssh_key_name "
            "because it does not supply a root password upon building the server."
        )

    ssh_interface = config.get_cloud_config_value(
        "ssh_interface", vm_, __opts__, search_global=False, default="public"
    )

    if ssh_interface in ["private", "public"]:
        log.info("ssh_interface: Setting interface for ssh to %s", ssh_interface)
        kwargs["ssh_interface"] = ssh_interface
    else:
        raise SaltCloudConfigError(
            "The DigitalOcean driver requires ssh_interface to be defined as 'public'"
            " or 'private'."
        )

    vpc_name = config.get_cloud_config_value(
        "vpc_name",
        vm_,
        __opts__,
        search_global=False,
        default=None,
    )

    if vpc_name is not None:
        vpc = _get_vpc_by_name(vpc_name)
        if vpc is None:
            raise SaltCloudConfigError("Invalid VPC name provided")
        else:
            kwargs["vpc_uuid"] = vpc[vpc_name]["id"]
    else:
        private_networking = config.get_cloud_config_value(
            "private_networking",
            vm_,
            __opts__,
            search_global=False,
            default=None,
        )
        if private_networking is not None:
            if not isinstance(private_networking, bool):
                raise SaltCloudConfigError(
                    "'private_networking' should be a boolean value."
                )
            kwargs["private_networking"] = private_networking

        if not private_networking and ssh_interface == "private":
            raise SaltCloudConfigError(
                "The DigitalOcean driver requires ssh_interface if defined as 'private' "
                "then private_networking should be set as 'True'."
            )
    backups_enabled = config.get_cloud_config_value(
        "backups_enabled",
        vm_,
        __opts__,
        search_global=False,
        default=None,
    )

    if backups_enabled is not None:
        if not isinstance(backups_enabled, bool):
            raise SaltCloudConfigError("'backups_enabled' should be a boolean value.")
        kwargs["backups"] = backups_enabled

    ipv6 = config.get_cloud_config_value(
        "ipv6",
        vm_,
        __opts__,
        search_global=False,
        default=None,
    )

    if ipv6 is not None:
        if not isinstance(ipv6, bool):
            raise SaltCloudConfigError("'ipv6' should be a boolean value.")
        kwargs["ipv6"] = ipv6

    monitoring = config.get_cloud_config_value(
        "monitoring",
        vm_,
        __opts__,
        search_global=False,
        default=None,
    )

    if monitoring is not None:
        if not isinstance(monitoring, bool):
            raise SaltCloudConfigError("'monitoring' should be a boolean value.")
        kwargs["monitoring"] = monitoring

    kwargs["tags"] = config.get_cloud_config_value(
        "tags", vm_, __opts__, search_global=False, default=False
    )

    userdata_file = config.get_cloud_config_value(
        "userdata_file", vm_, __opts__, search_global=False, default=None
    )
    if userdata_file is not None:
        try:
            with salt.utils.files.fopen(userdata_file, "r") as fp_:
                kwargs["user_data"] = salt.utils.cloud.userdata_template(
                    __opts__, vm_, salt.utils.stringutils.to_unicode(fp_.read())
                )
        except Exception as exc:  # pylint: disable=broad-except
            log.exception("Failed to read userdata from %s: %s", userdata_file, exc)

    create_dns_record = config.get_cloud_config_value(
        "create_dns_record",
        vm_,
        __opts__,
        search_global=False,
        default=None,
    )

    if create_dns_record:
        log.info("create_dns_record: will attempt to write DNS records")
        default_dns_domain = None
        dns_domain_name = vm_["name"].split(".")
        if len(dns_domain_name) > 2:
            log.debug(
                "create_dns_record: inferring default dns_hostname, dns_domain from"
                " minion name as FQDN"
            )
            default_dns_hostname = ".".join(dns_domain_name[:-2])
            default_dns_domain = ".".join(dns_domain_name[-2:])
        else:
            log.debug("create_dns_record: can't infer dns_domain from %s", vm_["name"])
            default_dns_hostname = dns_domain_name[0]

        dns_hostname = config.get_cloud_config_value(
            "dns_hostname",
            vm_,
            __opts__,
            search_global=False,
            default=default_dns_hostname,
        )
        dns_domain = config.get_cloud_config_value(
            "dns_domain",
            vm_,
            __opts__,
            search_global=False,
            default=default_dns_domain,
        )
        if dns_hostname and dns_domain:
            log.info(
                'create_dns_record: using dns_hostname="%s", dns_domain="%s"',
                dns_hostname,
                dns_domain,
            )

            def __add_dns_addr__(t, d):
                return post_dns_record(
                    dns_domain=dns_domain,
                    name=dns_hostname,
                    record_type=t,
                    record_data=d,
                )

            log.debug("create_dns_record: %s", __add_dns_addr__)
        else:
            log.error(
                "create_dns_record: could not determine dns_hostname and/or dns_domain"
            )
            raise SaltCloudConfigError(
                "'create_dns_record' must be a dict specifying \"domain\" "
                'and "hostname" or the minion name must be an FQDN.'
            )

    __utils__["cloud.fire_event"](
        "event",
        "requesting instance",
        "salt/cloud/{}/requesting".format(vm_["name"]),
        args=__utils__["cloud.filter_event"]("requesting", kwargs, list(kwargs)),
        sock_dir=__opts__["sock_dir"],
        transport=__opts__["transport"],
    )

    try:
        ret = create_node(kwargs)
    except Exception as exc:  # pylint: disable=broad-except
        log.error(
            "Error creating %s on DIGITALOCEAN\n\n"
            "The following exception was thrown when trying to "
            "run the initial deployment: %s",
            vm_["name"],
            exc,
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return False

    def __query_node_data(vm_name):
        data = show_instance(vm_name, "action")
        if not data:
            # Trigger an error in the wait_for_ip function
            return False
        if data["networks"].get("v4"):
            for network in data["networks"]["v4"]:
                if network["type"] == "public":
                    return data
        return False

    try:
        data = salt.utils.cloud.wait_for_ip(
            __query_node_data,
            update_args=(vm_["name"],),
            timeout=config.get_cloud_config_value(
                "wait_for_ip_timeout", vm_, __opts__, default=10 * 60
            ),
            interval=config.get_cloud_config_value(
                "wait_for_ip_interval", vm_, __opts__, default=10
            ),
        )
    except (SaltCloudExecutionTimeout, SaltCloudExecutionFailure) as exc:
        try:
            # It might be already up, let's destroy it!
            destroy(vm_["name"])
        except SaltCloudSystemExit:
            pass
        finally:
            raise SaltCloudSystemExit(str(exc))

    if not vm_.get("ssh_host"):
        vm_["ssh_host"] = None

    # add DNS records, set ssh_host, default to first found IP, preferring IPv4 for ssh bootstrap script target
    addr_families, dns_arec_types = (("v4", "v6"), ("A", "AAAA"))
    arec_map = dict(list(zip(addr_families, dns_arec_types)))
    for facing, addr_family, ip_address in [
        (net["type"], family, net["ip_address"])
        for family in addr_families
        for net in data["networks"][family]
    ]:
        log.info('found %s IP%s interface for "%s"', facing, addr_family, ip_address)
        dns_rec_type = arec_map[addr_family]
        if facing == "public":
            if create_dns_record:
                __add_dns_addr__(dns_rec_type, ip_address)
        if facing == ssh_interface:
            if not vm_["ssh_host"]:
                vm_["ssh_host"] = ip_address

    if vm_["ssh_host"] is None:
        raise SaltCloudSystemExit(
            "No suitable IP addresses found for ssh minion bootstrapping: {}".format(
                repr(data["networks"])
            )
        )

    log.debug(
        "Found public IP address to use for ssh minion bootstrapping: %s",
        vm_["ssh_host"],
    )

    vm_["key_filename"] = key_filename
    ret = __utils__["cloud.bootstrap"](vm_, __opts__)
    ret.update(data)

    log.info("Created Cloud VM '%s'", vm_["name"])
    log.debug("'%s' VM creation details:\n%s", vm_["name"], pprint.pformat(data))

    __utils__["cloud.fire_event"](
        "event",
        "created instance",
        "salt/cloud/{}/created".format(vm_["name"]),
        args=__utils__["cloud.filter_event"](
            "created", vm_, ["name", "profile", "provider", "driver"]
        ),
        sock_dir=__opts__["sock_dir"],
        transport=__opts__["transport"],
    )

    return ret


def query(
    method="droplets", droplet_id=None, command=None, args=None, http_method="get"
):
    """
    Make a web call to DigitalOcean
    """
    base_path = str(
        config.get_cloud_config_value(
            "api_root",
            get_configured_provider(),
            __opts__,
            search_global=False,
            default="https://api.digitalocean.com/v2",
        )
    )
    # vpcs method doesn't like the / at the end.
    if method == "vpcs":
        path = f"{base_path}/{method}"
    else:
        path = f"{base_path}/{method}/"

    if droplet_id:
        path += f"{droplet_id}/"

    if command:
        path += command

    if not isinstance(args, dict):
        args = {}

    personal_access_token = config.get_cloud_config_value(
        "personal_access_token",
        get_configured_provider(),
        __opts__,
        search_global=False,
    )

    data = salt.utils.json.dumps(args)

    requester = getattr(requests, http_method)
    request = requester(
        path,
        data=data,
        headers={
            "Authorization": "Bearer " + personal_access_token,
            "Content-Type": "application/json",
        },
        timeout=120,
    )
    if request.status_code > 299:
        raise SaltCloudSystemExit(
            "An error occurred while querying DigitalOcean. HTTP Code: {}  "
            "Error: '{}'".format(
                request.status_code,
                # request.read()
                request.text,
            )
        )

    log.debug(request.url)

    # success without data
    if request.status_code == 204:
        return True

    content = request.text

    result = salt.utils.json.loads(content)
    if result.get("status", "").lower() == "error":
        raise SaltCloudSystemExit(pprint.pformat(result.get("error_message", {})))

    return result


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


def show_instance(name, call=None):
    """
    Show the details from DigitalOcean concerning a droplet
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The show_instance action must be called with -a or --action."
        )
    node = _get_node(name)
    __utils__["cloud.cache_node"](node, _get_active_provider_name(), __opts__)
    return node


def _get_node(name):
    attempts = 10
    while attempts >= 0:
        try:
            return list_nodes_full(for_output=False)[name]
        except KeyError:
            attempts -= 1
            log.debug(
                "Failed to get the data for node '%s'. Remaining attempts: %s",
                name,
                attempts,
            )
            # Just a little delay between attempts...
            time.sleep(0.5)
    return {}


def list_keypairs(call=None):
    """
    Return a dict of all available VM locations on the cloud provider with
    relevant data
    """
    if call != "function":
        log.error("The list_keypairs function must be called with -f or --function.")
        return False

    fetch = True
    page = 1
    ret = {}

    while fetch:
        items = query(
            method="account/keys",
            command="?page=" + str(page) + "&per_page=100",
        )

        for key_pair in items["ssh_keys"]:
            name = key_pair["name"]
            if name in ret:
                raise SaltCloudSystemExit(
                    "A duplicate key pair name, '{}', was found in DigitalOcean's "
                    "key pair list. Please change the key name stored by DigitalOcean. "
                    "Be sure to adjust the value of 'ssh_key_file' in your cloud "
                    "profile or provider configuration, if necessary.".format(name)
                )
            ret[name] = {}
            for item in key_pair.keys():
                ret[name][item] = str(key_pair[item])

        page += 1
        try:
            fetch = "next" in items["links"]["pages"]
        except KeyError:
            fetch = False

    return ret


def show_keypair(kwargs=None, call=None):
    """
    Show the details of an SSH keypair
    """
    if call != "function":
        log.error("The show_keypair function must be called with -f or --function.")
        return False

    if not kwargs:
        kwargs = {}

    if "keyname" not in kwargs:
        log.error("A keyname is required.")
        return False

    keypairs = list_keypairs(call="function")
    keyid = keypairs[kwargs["keyname"]]["id"]
    log.debug("Key ID is %s", keyid)

    details = query(method="account/keys", command=keyid)

    return details


def import_keypair(kwargs=None, call=None):
    """
    Upload public key to cloud provider.
    Similar to EC2 import_keypair.

    .. versionadded:: 2016.11.0

    kwargs
        file(mandatory): public key file-name
        keyname(mandatory): public key name in the provider
    """
    with salt.utils.files.fopen(kwargs["file"], "r") as public_key_filename:
        public_key_content = salt.utils.stringutils.to_unicode(
            public_key_filename.read()
        )

    digitalocean_kwargs = {"name": kwargs["keyname"], "public_key": public_key_content}

    created_result = create_key(digitalocean_kwargs, call=call)
    return created_result


def create_key(kwargs=None, call=None):
    """
    Upload a public key
    """
    if call != "function":
        log.error("The create_key function must be called with -f or --function.")
        return False

    try:
        result = query(
            method="account",
            command="keys",
            args={"name": kwargs["name"], "public_key": kwargs["public_key"]},
            http_method="post",
        )
    except KeyError:
        log.info("`name` and `public_key` arguments must be specified")
        return False

    return result


def remove_key(kwargs=None, call=None):
    """
    Delete public key
    """
    if call != "function":
        log.error("The create_key function must be called with -f or --function.")
        return False

    try:
        result = query(
            method="account", command="keys/" + kwargs["id"], http_method="delete"
        )
    except KeyError:
        log.info("`id` argument must be specified")
        return False

    return result


def get_keyid(keyname):
    """
    Return the ID of the keyname
    """
    if not keyname:
        return None
    keypairs = list_keypairs(call="function")
    keyid = keypairs[keyname]["id"]
    if keyid:
        return keyid
    raise SaltCloudNotFound("The specified ssh key could not be found.")


def destroy(name, call=None):
    """
    Destroy a node. Will check termination protection and warn if enabled.

    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."
        )

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

    data = show_instance(name, call="action")
    node = query(method="droplets", droplet_id=data["id"], http_method="delete")

    ## This is all terribly optomistic:
    # vm_ = get_vm_config(name=name)
    # delete_dns_record = config.get_cloud_config_value(
    #     'delete_dns_record', vm_, __opts__, search_global=False, default=None,
    # )
    # TODO: when _vm config data can be made available, we should honor the configuration settings,
    # but until then, we should assume stale DNS records are bad, and default behavior should be to
    # delete them if we can. When this is resolved, also resolve the comments a couple of lines below.
    delete_dns_record = True

    if not isinstance(delete_dns_record, bool):
        raise SaltCloudConfigError("'delete_dns_record' should be a boolean value.")
    # When the "to do" a few lines up is resolved, remove these lines and use the if/else logic below.
    log.debug("Deleting DNS records for %s.", name)
    destroy_dns_records(name)

    # Until the "to do" from line 754 is taken care of, we don't need this logic.
    # if delete_dns_record:
    #    log.debug('Deleting DNS records for %s.', name)
    #    destroy_dns_records(name)
    # else:
    #    log.debug('delete_dns_record : %s', delete_dns_record)
    #    for line in pprint.pformat(dir()).splitlines():
    #       log.debug('delete context: %s', line)

    __utils__["cloud.fire_event"](
        "event",
        "destroyed instance",
        f"salt/cloud/{name}/destroyed",
        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 node


def post_dns_record(**kwargs):
    """
    Creates a DNS record for the given name if the domain is managed with DO.
    """
    if "kwargs" in kwargs:  # flatten kwargs if called via salt-cloud -f
        f_kwargs = kwargs["kwargs"]
        del kwargs["kwargs"]
        kwargs.update(f_kwargs)
    mandatory_kwargs = ("dns_domain", "name", "record_type", "record_data")
    for i in mandatory_kwargs:
        if kwargs[i]:
            pass
        else:
            error = '{}="{}" ## all mandatory args must be provided: {}'.format(
                i, kwargs[i], mandatory_kwargs
            )
            raise SaltInvocationError(error)

    domain = query(method="domains", droplet_id=kwargs["dns_domain"])

    if domain:
        result = query(
            method="domains",
            droplet_id=kwargs["dns_domain"],
            command="records",
            args={
                "type": kwargs["record_type"],
                "name": kwargs["name"],
                "data": kwargs["record_data"],
            },
            http_method="post",
        )
        return result

    return False


def destroy_dns_records(fqdn):
    """
    Deletes DNS records for the given hostname if the domain is managed with DO.
    """
    domain = ".".join(fqdn.split(".")[-2:])
    hostname = ".".join(fqdn.split(".")[:-2])
    # TODO: remove this when the todo on 754 is available
    try:
        response = query(method="domains", droplet_id=domain, command="records")
    except SaltCloudSystemExit:
        log.debug("Failed to find domains.")
        return False
    log.debug("found DNS records: %s", pprint.pformat(response))
    records = response["domain_records"]

    if records:
        record_ids = [r["id"] for r in records if r["name"] == hostname]
        log.debug("deleting DNS record IDs: %s", record_ids)
        for id_ in record_ids:
            try:
                log.info("deleting DNS record %s", id_)
                ret = query(
                    method="domains",
                    droplet_id=domain,
                    command=f"records/{id_}",
                    http_method="delete",
                )
            except SaltCloudSystemExit:
                log.error(
                    "failed to delete DNS domain %s record ID %s.", domain, hostname
                )
            log.debug("DNS deletion REST call returned: %s", pprint.pformat(ret))

    return False


def show_pricing(kwargs=None, call=None):
    """
    Show pricing for a particular profile. This is only an estimate, based on
    unofficial pricing sources.

    .. versionadded:: 2015.8.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f show_pricing my-digitalocean-config profile=my-profile
    """
    profile = __opts__["profiles"].get(kwargs["profile"], {})
    if not profile:
        return {"Error": "The requested profile was not found"}

    # Make sure the profile belongs to DigitalOcean
    provider = profile.get("provider", "0:0")
    comps = provider.split(":")
    if len(comps) < 2 or comps[1] != "digitalocean":
        return {"Error": "The requested profile does not belong to DigitalOcean"}

    raw = {}
    ret = {}
    sizes = avail_sizes()
    ret["per_hour"] = decimal.Decimal(sizes[profile["size"]]["price_hourly"])

    ret["per_day"] = ret["per_hour"] * 24
    ret["per_week"] = ret["per_day"] * 7
    ret["per_month"] = decimal.Decimal(sizes[profile["size"]]["price_monthly"])
    ret["per_year"] = ret["per_week"] * 52

    if kwargs.get("raw", False):
        ret["_raw"] = raw

    return {profile["profile"]: ret}


def list_floating_ips(call=None):
    """
    Return a list of the floating ips that are on the provider

    .. versionadded:: 2016.3.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f list_floating_ips my-digitalocean-config
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The list_floating_ips function must be called with "
            "-f or --function, or with the --list-floating-ips option"
        )

    fetch = True
    page = 1
    ret = {}

    while fetch:
        items = query(
            method="floating_ips",
            command="?page=" + str(page) + "&per_page=200",
        )

        for floating_ip in items["floating_ips"]:
            ret[floating_ip["ip"]] = {}
            for item in floating_ip.keys():
                ret[floating_ip["ip"]][item] = floating_ip[item]

        page += 1
        try:
            fetch = "next" in items["links"]["pages"]
        except KeyError:
            fetch = False

    return ret


def show_floating_ip(kwargs=None, call=None):
    """
    Show the details of a floating IP

    .. versionadded:: 2016.3.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f show_floating_ip my-digitalocean-config floating_ip='45.55.96.47'
    """
    if call != "function":
        log.error("The show_floating_ip function must be called with -f or --function.")
        return False

    if not kwargs:
        kwargs = {}

    if "floating_ip" not in kwargs:
        log.error("A floating IP is required.")
        return False

    floating_ip = kwargs["floating_ip"]
    log.debug("Floating ip is %s", floating_ip)

    details = query(method="floating_ips", command=floating_ip)

    return details


def create_floating_ip(kwargs=None, call=None):
    """
    Create a new floating IP

    .. versionadded:: 2016.3.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f create_floating_ip my-digitalocean-config region='NYC2'

        salt-cloud -f create_floating_ip my-digitalocean-config droplet_id='1234567'
    """
    if call != "function":
        log.error(
            "The create_floating_ip function must be called with -f or --function."
        )
        return False

    if not kwargs:
        kwargs = {}

    if "droplet_id" in kwargs:
        result = query(
            method="floating_ips",
            args={"droplet_id": kwargs["droplet_id"]},
            http_method="post",
        )

        return result

    elif "region" in kwargs:
        result = query(
            method="floating_ips", args={"region": kwargs["region"]}, http_method="post"
        )

        return result

    else:
        log.error("A droplet_id or region is required.")
        return False


def delete_floating_ip(kwargs=None, call=None):
    """
    Delete a floating IP

    .. versionadded:: 2016.3.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f delete_floating_ip my-digitalocean-config floating_ip='45.55.96.47'
    """
    if call != "function":
        log.error(
            "The delete_floating_ip function must be called with -f or --function."
        )
        return False

    if not kwargs:
        kwargs = {}

    if "floating_ip" not in kwargs:
        log.error("A floating IP is required.")
        return False

    floating_ip = kwargs["floating_ip"]
    log.debug("Floating ip is %s", kwargs["floating_ip"])

    result = query(method="floating_ips", command=floating_ip, http_method="delete")

    return result


def assign_floating_ip(kwargs=None, call=None):
    """
    Assign a floating IP

    .. versionadded:: 2016.3.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f assign_floating_ip my-digitalocean-config droplet_id=1234567 floating_ip='45.55.96.47'
    """
    if call != "function":
        log.error(
            "The assign_floating_ip function must be called with -f or --function."
        )
        return False

    if not kwargs:
        kwargs = {}

    if "floating_ip" and "droplet_id" not in kwargs:
        log.error("A floating IP and droplet_id is required.")
        return False

    result = query(
        method="floating_ips",
        command=kwargs["floating_ip"] + "/actions",
        args={"droplet_id": kwargs["droplet_id"], "type": "assign"},
        http_method="post",
    )

    return result


def unassign_floating_ip(kwargs=None, call=None):
    """
    Unassign a floating IP

    .. versionadded:: 2016.3.0

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f unassign_floating_ip my-digitalocean-config floating_ip='45.55.96.47'
    """
    if call != "function":
        log.error(
            "The inassign_floating_ip function must be called with -f or --function."
        )
        return False

    if not kwargs:
        kwargs = {}

    if "floating_ip" not in kwargs:
        log.error("A floating IP is required.")
        return False

    result = query(
        method="floating_ips",
        command=kwargs["floating_ip"] + "/actions",
        args={"type": "unassign"},
        http_method="post",
    )

    return result


def _get_vpc_by_name(name):
    """
    Helper function to format and parse vpc data. It's pretty expensive as it
    retrieves a list of vpcs and iterates through them till it finds the correct
    vpc by name.
    """
    fetch = True
    page = 1
    ret = {}

    log.debug("Matching vpc name with: %s", name)
    while fetch:
        items = query(method="vpcs", command=f"?page={str(page)}&per_page=200")
        for node in items["vpcs"]:
            log.debug("Node returned : %s", node["name"])
            if name == node["name"]:
                log.debug("Matched VPC node")
                ret[name] = {
                    "id": node["id"],
                    "urn": node["urn"],
                    "name": name,
                    "description": node["description"],
                    "region": node["region"],
                    "ip_range": node["ip_range"],
                    "default": node["default"],
                }
                return ret
        page += 1
        try:
            fetch = "next" in items["links"]["pages"]
        except KeyError:
            fetch = False
    return None


def _list_nodes(full=False, for_output=False):
    """
    Helper function to format and parse node data.
    """
    fetch = True
    page = 1
    ret = {}

    while fetch:
        items = query(method="droplets", command=f"?page={str(page)}&per_page=200")
        for node in items["droplets"]:
            name = node["name"]
            ret[name] = {}
            if full:
                ret[name] = _get_full_output(node, for_output=for_output)
            else:
                public_ips, private_ips = _get_ips(node["networks"])
                ret[name] = {
                    "id": node["id"],
                    "image": node["image"]["name"],
                    "name": name,
                    "private_ips": private_ips,
                    "public_ips": public_ips,
                    "size": node["size_slug"],
                    "state": str(node["status"]),
                }

        page += 1
        try:
            fetch = "next" in items["links"]["pages"]
        except KeyError:
            fetch = False

    return ret


def reboot(name, call=None):
    """
    Reboot a droplet in DigitalOcean.

    .. versionadded:: 2015.8.8

    name
        The name of the droplet to restart.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a reboot droplet_name
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The reboot action must be called with -a or --action."
        )

    data = show_instance(name, call="action")
    if data.get("status") == "off":
        return {
            "success": True,
            "action": "stop",
            "status": "off",
            "msg": "Machine is already off.",
        }

    ret = query(
        droplet_id=data["id"],
        command="actions",
        args={"type": "reboot"},
        http_method="post",
    )

    return {
        "success": True,
        "action": ret["action"]["type"],
        "state": ret["action"]["status"],
    }


def start(name, call=None):
    """
    Start a droplet in DigitalOcean.

    .. versionadded:: 2015.8.8

    name
        The name of the droplet to start.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a start droplet_name
    """
    if call != "action":
        raise SaltCloudSystemExit(
            "The start action must be called with -a or --action."
        )

    data = show_instance(name, call="action")
    if data.get("status") == "active":
        return {
            "success": True,
            "action": "start",
            "status": "active",
            "msg": "Machine is already running.",
        }

    ret = query(
        droplet_id=data["id"],
        command="actions",
        args={"type": "power_on"},
        http_method="post",
    )

    return {
        "success": True,
        "action": ret["action"]["type"],
        "state": ret["action"]["status"],
    }


def stop(name, call=None):
    """
    Stop a droplet in DigitalOcean.

    .. versionadded:: 2015.8.8

    name
        The name of the droplet to stop.

    CLI Example:

    .. code-block:: bash

        salt-cloud -a stop droplet_name
    """
    if call != "action":
        raise SaltCloudSystemExit("The stop action must be called with -a or --action.")

    data = show_instance(name, call="action")
    if data.get("status") == "off":
        return {
            "success": True,
            "action": "stop",
            "status": "off",
            "msg": "Machine is already off.",
        }

    ret = query(
        droplet_id=data["id"],
        command="actions",
        args={"type": "shutdown"},
        http_method="post",
    )

    return {
        "success": True,
        "action": ret["action"]["type"],
        "state": ret["action"]["status"],
    }


def _get_full_output(node, for_output=False):
    """
    Helper function for _list_nodes to loop through all node information.
    Returns a dictionary containing the full information of a node.
    """
    ret = {}
    for item in node.keys():
        value = node[item]
        if value is not None and for_output:
            value = str(value)
        ret[item] = value
    return ret


def _get_ips(networks):
    """
    Helper function for list_nodes. Returns public and private ip lists based on a
    given network dictionary.
    """
    v4s = networks.get("v4")
    v6s = networks.get("v6")
    public_ips = []
    private_ips = []

    if v4s:
        for item in v4s:
            ip_type = item.get("type")
            ip_address = item.get("ip_address")
            if ip_type == "public":
                public_ips.append(ip_address)
            if ip_type == "private":
                private_ips.append(ip_address)

    if v6s:
        for item in v6s:
            ip_type = item.get("type")
            ip_address = item.get("ip_address")
            if ip_type == "public":
                public_ips.append(ip_address)
            if ip_type == "private":
                private_ips.append(ip_address)

    return public_ips, private_ips
