"""
Parallels Cloud Module
======================

The Parallels cloud module is used to control access to cloud providers using
the Parallels VPS system.

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

.. code-block:: yaml

    my-parallels-config:
      # Parallels account information
      user: myuser
      password: mypassword
      url: https://api.cloud.xmission.com:4465/paci/v1.0/
      driver: parallels

"""

import logging
import pprint
import time
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from urllib.error import URLError

import salt.config as config
import salt.utils.cloud
from salt.exceptions import (
    SaltCloudExecutionFailure,
    SaltCloudExecutionTimeout,
    SaltCloudNotFound,
    SaltCloudSystemExit,
)

log = logging.getLogger(__name__)

__virtualname__ = "parallels"


# Only load in this module if the PARALLELS configurations are in place
def __virtual__():
    """
    Check for PARALLELS configurations
    """
    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__,
        (
            "user",
            "password",
            "url",
        ),
    )


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

    items = query(action="template")
    ret = {}
    for item in items:
        ret[item.attrib["name"]] = item.attrib

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

    ret = {}
    items = query(action="ve")

    for item in items:
        name = item.attrib["name"]
        node = show_instance(name, call="action")

        ret[name] = {
            "id": node["id"],
            "image": node["platform"]["template-info"]["name"],
            "state": node["state"],
        }
        if "private-ip" in node["network"]:
            ret[name]["private_ips"] = [node["network"]["private-ip"]]
        if "public-ip" in node["network"]:
            ret[name]["public_ips"] = [node["network"]["public-ip"]]

    return ret


def list_nodes_full(call=None):
    """
    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."
        )

    ret = {}
    items = query(action="ve")

    for item in items:
        name = item.attrib["name"]
        node = show_instance(name, call="action")

        ret[name] = node
        ret[name]["image"] = node["platform"]["template-info"]["name"]
        if "private-ip" in node["network"]:
            ret[name]["private_ips"] = [node["network"]["private-ip"]["address"]]
        if "public-ip" in node["network"]:
            ret[name]["public_ips"] = [node["network"]["public-ip"]["address"]]

    return ret


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(),
        __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
    )
    for image in images:
        if str(vm_image) in (images[image]["name"], images[image]["id"]):
            return images[image]["id"]
    raise SaltCloudNotFound("The specified image could not be found.")


def create_node(vm_):
    """
    Build and submit the XML to create a node
    """
    # Start the tree
    content = ET.Element("ve")

    # Name of the instance
    name = ET.SubElement(content, "name")
    name.text = vm_["name"]

    # Description, defaults to name
    desc = ET.SubElement(content, "description")
    desc.text = config.get_cloud_config_value(
        "desc", vm_, __opts__, default=vm_["name"], search_global=False
    )

    # How many CPU cores, and how fast they are
    cpu = ET.SubElement(content, "cpu")
    cpu.attrib["number"] = config.get_cloud_config_value(
        "cpu_number", vm_, __opts__, default="1", search_global=False
    )
    cpu.attrib["power"] = config.get_cloud_config_value(
        "cpu_power", vm_, __opts__, default="1000", search_global=False
    )

    # How many megabytes of RAM
    ram = ET.SubElement(content, "ram-size")
    ram.text = config.get_cloud_config_value(
        "ram", vm_, __opts__, default="256", search_global=False
    )

    # Bandwidth available, in kbps
    bandwidth = ET.SubElement(content, "bandwidth")
    bandwidth.text = config.get_cloud_config_value(
        "bandwidth", vm_, __opts__, default="100", search_global=False
    )

    # How many public IPs will be assigned to this instance
    ip_num = ET.SubElement(content, "no-of-public-ip")
    ip_num.text = config.get_cloud_config_value(
        "ip_num", vm_, __opts__, default="1", search_global=False
    )

    # Size of the instance disk
    disk = ET.SubElement(content, "ve-disk")
    disk.attrib["local"] = "true"
    disk.attrib["size"] = config.get_cloud_config_value(
        "disk_size", vm_, __opts__, default="10", search_global=False
    )

    # Attributes for the image
    vm_image = config.get_cloud_config_value(
        "image", vm_, __opts__, search_global=False
    )
    image = show_image({"image": vm_image}, call="function")
    platform = ET.SubElement(content, "platform")
    template = ET.SubElement(platform, "template-info")
    template.attrib["name"] = vm_image
    os_info = ET.SubElement(platform, "os-info")
    os_info.attrib["technology"] = image[vm_image]["technology"]
    os_info.attrib["type"] = image[vm_image]["osType"]

    # Username and password
    admin = ET.SubElement(content, "admin")
    admin.attrib["login"] = config.get_cloud_config_value(
        "ssh_username", vm_, __opts__, default="root"
    )
    admin.attrib["password"] = config.get_cloud_config_value(
        "password", vm_, __opts__, search_global=False
    )

    data = ET.tostring(content, encoding="UTF-8")

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

    node = query(action="ve", method="POST", data=data)
    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 "parallels",
                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"])

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

    name = vm_["name"]
    if not wait_until(name, "CREATED"):
        return {"Error": f"Unable to start {name}, command timed out"}
    start(vm_["name"], call="action")

    if not wait_until(name, "STARTED"):
        return {"Error": f"Unable to start {name}, command timed out"}

    def __query_node_data(vm_name):
        data = show_instance(vm_name, call="action")
        if "public-ip" not in data["network"]:
            # Trigger another iteration
            return
        return data

    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=5 * 60
            ),
            interval=config.get_cloud_config_value(
                "wait_for_ip_interval", vm_, __opts__, default=5
            ),
        )
    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))

    comps = data["network"]["public-ip"]["address"].split("/")
    public_ip = comps[0]

    vm_["ssh_host"] = public_ip
    ret = __utils__["cloud.bootstrap"](vm_, __opts__)

    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 data


def query(action=None, command=None, args=None, method="GET", data=None):
    """
    Make a web call to a Parallels provider
    """
    path = config.get_cloud_config_value(
        "url", get_configured_provider(), __opts__, search_global=False
    )
    auth_handler = urllib.request.HTTPBasicAuthHandler()
    auth_handler.add_password(
        realm="Parallels Instance Manager",
        uri=path,
        user=config.get_cloud_config_value(
            "user", get_configured_provider(), __opts__, search_global=False
        ),
        passwd=config.get_cloud_config_value(
            "password", get_configured_provider(), __opts__, search_global=False
        ),
    )
    opener = urllib.request.build_opener(auth_handler)
    urllib.request.install_opener(opener)

    if action:
        path += action

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

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

    kwargs = {"data": data}
    if isinstance(data, str) and "<?xml" in data:
        kwargs["headers"] = {
            "Content-type": "application/xml",
        }

    if args:
        params = urllib.parse.urlencode(args)
        req = urllib.request.Request(url=f"{path}?{params}", **kwargs)
    else:
        req = urllib.request.Request(url=path, **kwargs)

    req.get_method = lambda: method

    log.debug("%s %s", method, req.get_full_url())
    if data:
        log.debug(data)

    try:
        result = urllib.request.urlopen(req)
        log.debug("PARALLELS Response Status Code: %s", result.getcode())

        if "content-length" in result.headers:
            content = result.read()
            result.close()
            items = ET.fromstring(content)
            return items

        return {}
    except URLError as exc:
        root = ET.fromstring(exc.read())
        log.error("PARALLELS Response Status Code: %s %s\n%s", exc.code, exc.msg, root)
        return {"error": root}


def script(vm_):
    """
    Return the script deployment object
    """
    return 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_)
        ),
    )


def show_image(kwargs, call=None):
    """
    Show the details from Parallels concerning an image
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The show_image function must be called with -f or --function."
        )

    items = query(action="template", command=kwargs["image"])
    if "error" in items:
        return items["error"]

    ret = {}
    for item in items:
        ret.update({item.attrib["name"]: item.attrib})

    return ret


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

    items = query(action="ve", command=name)

    ret = {}
    for item in items:
        if "text" in item.__dict__:
            ret[item.tag] = item.text
        else:
            ret[item.tag] = item.attrib

        if item._children:
            ret[item.tag] = {}
            children = item._children
            for child in children:
                ret[item.tag][child.tag] = child.attrib

    __utils__["cloud.cache_node"](ret, _get_active_provider_name(), __opts__)
    return ret


def wait_until(name, state, timeout=300):
    """
    Wait until a specific state has been reached on  a node
    """
    start_time = time.time()
    node = show_instance(name, call="action")
    while True:
        if node["state"] == state:
            return True
        time.sleep(1)
        if time.time() - start_time > timeout:
            return False
        node = show_instance(name, call="action")


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

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

    node = show_instance(name, call="action")
    if node["state"] == "STARTED":
        stop(name, call="action")
        if not wait_until(name, "STOPPED"):
            return {"Error": f"Unable to destroy {name}, command timed out"}

    data = query(action="ve", command=name, method="DELETE")

    if "error" in data:
        return data["error"]

    __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 {"Destroyed": f"{name} was destroyed."}


def start(name, call=None):
    """
    Start a node.

    CLI Example:

    .. code-block:: bash

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

    data = query(action="ve", command=f"{name}/start", method="PUT")

    if "error" in data:
        return data["error"]

    return {"Started": f"{name} was started."}


def stop(name, call=None):
    """
    Stop a node.

    CLI Example:

    .. code-block:: bash

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

    data = query(action="ve", command=f"{name}/stop", method="PUT")

    if "error" in data:
        return data["error"]

    return {"Stopped": f"{name} was stopped."}
