"""
GoGrid Cloud Module
====================

The GoGrid cloud module. This module interfaces with the gogrid public cloud
service. To use Salt Cloud with GoGrid log into the GoGrid web interface and
create an api key. Do this by clicking on "My Account" and then going to the
API Keys tab.

Set up the cloud configuration at ``/etc/salt/cloud.providers`` or
``/etc/salt/cloud.providers.d/gogrid.conf``:

.. code-block:: yaml

    my-gogrid-config:
      # The generated api key to use
      apikey: asdff7896asdh789
      # The apikey's shared secret
      sharedsecret: saltybacon
      driver: gogrid

.. note::

    A Note about using Map files with GoGrid:

    Due to limitations in the GoGrid API, instances cannot be provisioned in parallel
    with the GoGrid driver. Map files will work with GoGrid, but the ``-P``
    argument should not be used on maps referencing GoGrid instances.

.. note::

    A Note about using Map files with GoGrid:

    Due to limitations in the GoGrid API, instances cannot be provisioned in parallel
    with the GoGrid driver. Map files will work with GoGrid, but the ``-P``
    argument should not be used on maps referencing GoGrid instances.

"""

import logging
import pprint
import time

import salt.config as config
import salt.utils.cloud
import salt.utils.hashutils
from salt.exceptions import SaltCloudException, SaltCloudSystemExit

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

__virtualname__ = "gogrid"


# Only load in this module if the GoGrid configurations are in place
def __virtual__():
    """
    Check for GoGrid configs
    """
    if get_configured_provider() 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__,
        _get_active_provider_name() or __virtualname__,
        ("apikey", "sharedsecret"),
    )


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 "gogrid",
                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"],
    )

    if len(vm_["name"]) > 20:
        raise SaltCloudException("VM names must not be longer than 20 characters")

    log.info("Creating Cloud VM %s", vm_["name"])
    image_id = avail_images()[vm_["image"]]["id"]
    if "assign_public_ip" in vm_:
        host_ip = vm_["assign_public_ip"]
    else:
        public_ips = list_public_ips()
        if not public_ips:
            raise SaltCloudException("No more IPs available")
        host_ip = next(iter(public_ips))

    create_kwargs = {
        "name": vm_["name"],
        "image": image_id,
        "ram": vm_["size"],
        "ip": host_ip,
    }

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

    try:
        data = _query("grid", "server/add", args=create_kwargs)
    except Exception:  # pylint: disable=broad-except
        log.error(
            "Error creating %s on GOGRID\n\n"
            "The following exception was thrown when trying to "
            "run the initial deployment:\n",
            vm_["name"],
            # Show the traceback if the debug logging level is enabled
            exc_info_on_loglevel=logging.DEBUG,
        )
        return False

    ssh_username = config.get_cloud_config_value(
        "ssh_username", vm_, __opts__, default="root"
    )

    def wait_for_apipass():
        """
        Wait for the password to become available, via the API
        """
        try:
            passwords = list_passwords()
            return passwords[vm_["name"]][0]["password"]
        except KeyError:
            pass
        time.sleep(5)
        return False

    vm_["password"] = salt.utils.cloud.wait_for_fun(
        wait_for_apipass,
        timeout=config.get_cloud_config_value(
            "wait_for_fun_timeout", vm_, __opts__, default=15 * 60
        ),
    )

    vm_["ssh_host"] = host_ip
    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 list_nodes(full=False, call=None):
    """
    List of nodes, keeping only a brief listing

    CLI Example:

    .. code-block:: bash

        salt-cloud -Q
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The list_nodes function must be called with -f or --function."
        )

    ret = {}
    nodes = list_nodes_full("function")
    if full:
        return nodes

    for node in nodes:
        ret[node] = {}
        for item in ("id", "image", "size", "public_ips", "private_ips", "state"):
            ret[node][item] = nodes[node][item]

    return ret


def list_nodes_full(call=None):
    """
    List nodes, with all available information

    CLI Example:

    .. code-block:: bash

        salt-cloud -F
    """
    response = _query("grid", "server/list")

    ret = {}
    for item in response["list"]:
        name = item["name"]
        ret[name] = item

        ret[name]["image_info"] = item["image"]
        ret[name]["image"] = item["image"]["friendlyName"]
        ret[name]["size"] = item["ram"]["name"]
        ret[name]["public_ips"] = [item["ip"]["ip"]]
        ret[name]["private_ips"] = []
        ret[name]["state_info"] = item["state"]
        if "active" in item["state"]["description"]:
            ret[name]["state"] = "RUNNING"

    return ret


def list_nodes_select(call=None):
    """
    Return a list of the VMs that are on the provider, with select fields

    CLI Example:

    .. code-block:: bash

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


def avail_locations():
    """
    Available locations
    """
    response = list_common_lookups(kwargs={"lookup": "ip.datacenter"})

    ret = {}
    for item in response["list"]:
        name = item["name"]
        ret[name] = item

    return ret


def avail_sizes():
    """
    Available sizes
    """
    response = list_common_lookups(kwargs={"lookup": "server.ram"})

    ret = {}
    for item in response["list"]:
        name = item["name"]
        ret[name] = item

    return ret


def avail_images():
    """
    Available images
    """
    response = _query("grid", "image/list")

    ret = {}
    for item in response["list"]:
        name = item["friendlyName"]
        ret[name] = item

    return ret


def list_passwords(kwargs=None, call=None):
    """
    List all password on the account

    .. versionadded:: 2015.8.0
    """
    response = _query("support", "password/list")

    ret = {}
    for item in response["list"]:
        if "server" in item:
            server = item["server"]["name"]
            if server not in ret:
                ret[server] = []
            ret[server].append(item)

    return ret


def list_public_ips(kwargs=None, call=None):
    """
    List all available public IPs.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_public_ips <provider>

    To list unavailable (assigned) IPs, use:

    CLI Example:

    .. code-block:: bash

        salt-cloud -f list_public_ips <provider> state=assigned

    .. versionadded:: 2015.8.0
    """
    if kwargs is None:
        kwargs = {}

    args = {}
    if "state" in kwargs:
        if kwargs["state"] == "assigned":
            args["ip.state"] = "Assigned"
        else:
            args["ip.state"] = "Unassigned"
    else:
        args["ip.state"] = "Unassigned"

    args["ip.type"] = "Public"

    response = _query("grid", "ip/list", args=args)

    ret = {}
    for item in response["list"]:
        name = item["ip"]
        ret[name] = item

    return ret


def list_common_lookups(kwargs=None, call=None):
    """
    List common lookups for a particular type of item

    .. versionadded:: 2015.8.0
    """
    if kwargs is None:
        kwargs = {}

    args = {}
    if "lookup" in kwargs:
        args["lookup"] = kwargs["lookup"]

    response = _query("common", "lookup/list", args=args)

    return response


def destroy(name, call=None):
    """
    Destroy a machine by name

    CLI Example:

    .. code-block:: bash

        salt-cloud -d vm_name
    """
    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"],
    )

    response = _query("grid", "server/delete", args={"name": name})

    __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 response


def reboot(name, call=None):
    """
    Reboot a machine by name

    CLI Example:

    .. code-block:: bash

        salt-cloud -a reboot vm_name

    .. versionadded:: 2015.8.0
    """
    return _query("grid", "server/power", args={"name": name, "power": "restart"})


def stop(name, call=None):
    """
    Stop a machine by name

    CLI Example:

    .. code-block:: bash

        salt-cloud -a stop vm_name

    .. versionadded:: 2015.8.0
    """
    return _query("grid", "server/power", args={"name": name, "power": "stop"})


def start(name, call=None):
    """
    Start a machine by name

    CLI Example:

    .. code-block:: bash

        salt-cloud -a start vm_name

    .. versionadded:: 2015.8.0
    """
    return _query("grid", "server/power", args={"name": name, "power": "start"})


def show_instance(name, call=None):
    """
    Start a machine by name

    CLI Example:

    .. code-block:: bash

        salt-cloud -a show_instance vm_name

    .. versionadded:: 2015.8.0
    """
    response = _query("grid", "server/get", args={"name": name})
    ret = {}
    for item in response["list"]:
        name = item["name"]
        ret[name] = item

        ret[name]["image_info"] = item["image"]
        ret[name]["image"] = item["image"]["friendlyName"]
        ret[name]["size"] = item["ram"]["name"]
        ret[name]["public_ips"] = [item["ip"]["ip"]]
        ret[name]["private_ips"] = []
        ret[name]["state_info"] = item["state"]
        if "active" in item["state"]["description"]:
            ret[name]["state"] = "RUNNING"
    return ret


def _query(
    action=None, command=None, args=None, method="GET", header_dict=None, data=None
):
    """
    Make a web call to GoGrid

    .. versionadded:: 2015.8.0
    """
    vm_ = get_configured_provider()
    apikey = config.get_cloud_config_value("apikey", vm_, __opts__, search_global=False)
    sharedsecret = config.get_cloud_config_value(
        "sharedsecret", vm_, __opts__, search_global=False
    )

    path = "https://api.gogrid.com/api/"

    if action:
        path += action

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

    log.debug("GoGrid URL: %s", path)

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

    epoch = str(int(time.time()))
    hashtext = "".join((apikey, sharedsecret, epoch))
    args["sig"] = salt.utils.hashutils.md5_digest(hashtext)
    args["format"] = "json"
    args["v"] = "1.0"
    args["api_key"] = apikey

    if header_dict is None:
        header_dict = {}

    if method != "POST":
        header_dict["Accept"] = "application/json"

    decode = True
    if method == "DELETE":
        decode = False

    return_content = None
    result = salt.utils.http.query(
        path,
        method,
        params=args,
        data=data,
        header_dict=header_dict,
        decode=decode,
        decode_type="json",
        text=True,
        status=True,
        opts=__opts__,
    )
    log.debug("GoGrid Response Status Code: %s", result["status"])

    return result["dict"]
