"""
QingCloud Cloud Module
======================

.. versionadded:: 2015.8.0

The QingCloud cloud module is used to control access to the QingCloud.
http://www.qingcloud.com/

Use of this module requires the ``access_key_id``, ``secret_access_key``,
``zone`` and ``key_filename`` parameter to be set.

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

.. code-block:: yaml

    my-qingcloud:
      driver: qingcloud
      access_key_id: AKIDMRTGYONNLTFFRBQJ
      secret_access_key: clYwH21U5UOmcov4aNV2V2XocaHCG3JZGcxEczFu
      zone: pek2
      key_filename: /path/to/your.pem

:depends: requests
"""

import base64
import hmac
import logging
import pprint
import time
import urllib.parse
from hashlib import sha256

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

try:
    import requests

    HAS_REQUESTS = True
except ImportError:
    HAS_REQUESTS = False


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

__virtualname__ = "qingcloud"

DEFAULT_QINGCLOUD_API_VERSION = 1
DEFAULT_QINGCLOUD_SIGNATURE_VERSION = 1


# Only load in this module if the qingcloud configurations are in place
def __virtual__():
    """
    Check for QingCloud 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__,
        _get_active_provider_name() or __virtualname__,
        ("access_key_id", "secret_access_key", "zone", "key_filename"),
    )


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


def _compute_signature(parameters, access_key_secret, method, path):
    """
    Generate an API request signature. Detailed document can be found at:

    https://docs.qingcloud.com/api/common/signature.html
    """
    parameters["signature_method"] = "HmacSHA256"

    string_to_sign = f"{method.upper()}\n{path}\n"

    keys = sorted(parameters.keys())
    pairs = []
    for key in keys:
        val = str(parameters[key]).encode("utf-8")
        pairs.append(
            urllib.parse.quote(key, safe="") + "=" + urllib.parse.quote(val, safe="-_~")
        )
    qs = "&".join(pairs)
    string_to_sign += qs

    h = hmac.new(access_key_secret, digestmod=sha256)
    h.update(string_to_sign)

    signature = base64.b64encode(h.digest()).strip()

    return signature


def query(params=None):
    """
    Make a web call to QingCloud IaaS API.
    """
    path = "https://api.qingcloud.com/iaas/"

    access_key_id = config.get_cloud_config_value(
        "access_key_id", get_configured_provider(), __opts__, search_global=False
    )
    access_key_secret = config.get_cloud_config_value(
        "secret_access_key", get_configured_provider(), __opts__, search_global=False
    )

    verify_ssl = config.get_cloud_config_value(
        "verify_ssl",
        get_configured_provider(),
        __opts__,
        default=True,
        search_global=False,
    )

    # public interface parameters
    real_parameters = {
        "access_key_id": access_key_id,
        "signature_version": DEFAULT_QINGCLOUD_SIGNATURE_VERSION,
        "time_stamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
        "version": DEFAULT_QINGCLOUD_API_VERSION,
    }

    # include action or function parameters
    if params:
        for key, value in params.items():
            if isinstance(value, list):
                for i in range(1, len(value) + 1):
                    if isinstance(value[i - 1], dict):
                        for sk, sv in value[i - 1].items():
                            if isinstance(sv, dict) or isinstance(sv, list):
                                sv = salt.utils.json.dumps(sv, separators=(",", ":"))
                            real_parameters[f"{key}.{i}.{sk}"] = sv
                    else:
                        real_parameters[f"{key}.{i}"] = value[i - 1]
            else:
                real_parameters[key] = value

    # Calculate the string for Signature
    signature = _compute_signature(real_parameters, access_key_secret, "GET", "/iaas/")
    real_parameters["signature"] = signature

    # print('parameters:')
    # pprint.pprint(real_parameters)

    request = requests.get(path, params=real_parameters, verify=verify_ssl, timeout=120)

    # print('url:')
    # print(request.url)

    if request.status_code != 200:
        raise SaltCloudSystemExit(
            "An error occurred while querying QingCloud. HTTP Code: {}  "
            "Error: '{}'".format(request.status_code, request.text)
        )

    log.debug(request.url)

    content = request.text
    result = salt.utils.json.loads(content)

    # print('response:')
    # pprint.pprint(result)

    if result["ret_code"] != 0:
        raise SaltCloudSystemExit(pprint.pformat(result.get("message", {})))

    return result


def avail_locations(call=None):
    """
    Return a dict of all available locations on the provider with
    relevant data.

    CLI Examples:

    .. code-block:: bash

        salt-cloud --list-locations my-qingcloud
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_locations function must be called with "
            "-f or --function, or with the --list-locations option"
        )

    params = {
        "action": "DescribeZones",
    }
    items = query(params=params)

    result = {}
    for region in items["zone_set"]:
        result[region["zone_id"]] = {}
        for key in region:
            result[region["zone_id"]][key] = str(region[key])

    return result


def _get_location(vm_=None):
    """
    Return the VM's location. Used by create().
    """
    locations = avail_locations()

    vm_location = str(
        config.get_cloud_config_value("zone", vm_, __opts__, search_global=False)
    )

    if not vm_location:
        raise SaltCloudNotFound("No location specified for this VM.")

    if vm_location in locations:
        return vm_location

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


def _get_specified_zone(kwargs=None, provider=None):
    if provider is None:
        provider = get_configured_provider()

    if isinstance(kwargs, dict):
        zone = kwargs.get("zone", None)
        if zone is not None:
            return zone

    zone = provider["zone"]
    return zone


def avail_images(kwargs=None, call=None):
    """
    Return a list of the images that are on the provider.

    CLI Examples:

    .. code-block:: bash

        salt-cloud --list-images my-qingcloud
        salt-cloud -f avail_images my-qingcloud zone=gd1
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_images function must be called with "
            "-f or --function, or with the --list-images option"
        )

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

    params = {
        "action": "DescribeImages",
        "provider": "system",
        "zone": _get_specified_zone(kwargs, get_configured_provider()),
    }
    items = query(params=params)

    result = {}
    for image in items["image_set"]:
        result[image["image_id"]] = {}
        for key in image:
            result[image["image_id"]][key] = image[key]

    return result


def _get_image(vm_):
    """
    Return the VM's image. Used by create().
    """
    images = avail_images()
    vm_image = str(
        config.get_cloud_config_value("image", vm_, __opts__, search_global=False)
    )

    if not vm_image:
        raise SaltCloudNotFound("No image specified for this VM.")

    if vm_image in images:
        return vm_image

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


def show_image(kwargs, call=None):
    """
    Show the details from QingCloud concerning an image.

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f show_image my-qingcloud image=trustysrvx64c
        salt-cloud -f show_image my-qingcloud image=trustysrvx64c,coreos4
        salt-cloud -f show_image my-qingcloud image=trustysrvx64c zone=ap1
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The show_images function must be called with -f or --function"
        )

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

    images = kwargs["image"]
    images = images.split(",")

    params = {
        "action": "DescribeImages",
        "images": images,
        "zone": _get_specified_zone(kwargs, get_configured_provider()),
    }

    items = query(params=params)

    if not items["image_set"]:
        raise SaltCloudNotFound("The specified image could not be found.")

    result = {}
    for image in items["image_set"]:
        result[image["image_id"]] = {}
        for key in image:
            result[image["image_id"]][key] = image[key]

    return result


# QingCloud doesn't provide an API of geting instance sizes
QINGCLOUD_SIZES = {
    "pek2": {
        "c1m1": {"cpu": 1, "memory": "1G"},
        "c1m2": {"cpu": 1, "memory": "2G"},
        "c1m4": {"cpu": 1, "memory": "4G"},
        "c2m2": {"cpu": 2, "memory": "2G"},
        "c2m4": {"cpu": 2, "memory": "4G"},
        "c2m8": {"cpu": 2, "memory": "8G"},
        "c4m4": {"cpu": 4, "memory": "4G"},
        "c4m8": {"cpu": 4, "memory": "8G"},
        "c4m16": {"cpu": 4, "memory": "16G"},
    },
    "pek1": {
        "small_b": {"cpu": 1, "memory": "1G"},
        "small_c": {"cpu": 1, "memory": "2G"},
        "medium_a": {"cpu": 2, "memory": "2G"},
        "medium_b": {"cpu": 2, "memory": "4G"},
        "medium_c": {"cpu": 2, "memory": "8G"},
        "large_a": {"cpu": 4, "memory": "4G"},
        "large_b": {"cpu": 4, "memory": "8G"},
        "large_c": {"cpu": 4, "memory": "16G"},
    },
}
QINGCLOUD_SIZES["ap1"] = QINGCLOUD_SIZES["pek2"]
QINGCLOUD_SIZES["gd1"] = QINGCLOUD_SIZES["pek2"]


def avail_sizes(kwargs=None, call=None):
    """
    Return a list of the instance sizes that are on the provider.

    CLI Examples:

    .. code-block:: bash

        salt-cloud --list-sizes my-qingcloud
        salt-cloud -f avail_sizes my-qingcloud zone=pek2
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The avail_sizes function must be called with "
            "-f or --function, or with the --list-sizes option"
        )

    zone = _get_specified_zone(kwargs, get_configured_provider())

    result = {}
    for size_key in QINGCLOUD_SIZES[zone]:
        result[size_key] = {}
        for attribute_key in QINGCLOUD_SIZES[zone][size_key]:
            result[size_key][attribute_key] = QINGCLOUD_SIZES[zone][size_key][
                attribute_key
            ]

    return result


def _get_size(vm_):
    """
    Return the VM's size. Used by create().
    """
    sizes = avail_sizes()

    vm_size = str(
        config.get_cloud_config_value("size", vm_, __opts__, search_global=False)
    )

    if not vm_size:
        raise SaltCloudNotFound("No size specified for this instance.")

    if vm_size in sizes:
        return vm_size

    raise SaltCloudNotFound(f"The specified size, '{vm_size}', could not be found.")


def _show_normalized_node(full_node):
    """
    Normalize the QingCloud instance data. Used by list_nodes()-related
    functions.
    """
    public_ips = full_node.get("eip", [])
    if public_ips:
        public_ip = public_ips["eip_addr"]
        public_ips = [
            public_ip,
        ]

    private_ips = []
    for vxnet in full_node.get("vxnets", []):
        private_ip = vxnet.get("private_ip", None)
        if private_ip:
            private_ips.append(private_ip)

    normalized_node = {
        "id": full_node["instance_id"],
        "image": full_node["image"]["image_id"],
        "size": full_node["instance_type"],
        "state": full_node["status"],
        "private_ips": private_ips,
        "public_ips": public_ips,
    }

    return normalized_node


def list_nodes_full(call=None):
    """
    Return a list of the instances that are on the provider.

    CLI Examples:

    .. code-block:: bash

        salt-cloud -F my-qingcloud
    """
    if call == "action":
        raise SaltCloudSystemExit(
            "The list_nodes_full function must be called with -f or --function."
        )

    zone = _get_specified_zone()

    params = {
        "action": "DescribeInstances",
        "zone": zone,
        "status": ["pending", "running", "stopped", "suspended"],
    }
    items = query(params=params)

    log.debug("Total %s instances found in zone %s", items["total_count"], zone)

    result = {}

    if items["total_count"] == 0:
        return result

    for node in items["instance_set"]:
        normalized_node = _show_normalized_node(node)
        node.update(normalized_node)

        result[node["instance_id"]] = node

    provider = _get_active_provider_name() or "qingcloud"
    if ":" in provider:
        comps = provider.split(":")
        provider = comps[0]

    __opts__["update_cachedir"] = True
    __utils__["cloud.cache_node_list"](result, provider, __opts__)

    return result


def list_nodes(call=None):
    """
    Return a list of the instances that are on the provider.

    CLI Examples:

    .. code-block:: bash

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

    nodes = list_nodes_full()

    ret = {}
    for instance_id, full_node in nodes.items():
        ret[instance_id] = {
            "id": full_node["id"],
            "image": full_node["image"],
            "size": full_node["size"],
            "state": full_node["state"],
            "public_ips": full_node["public_ips"],
            "private_ips": full_node["private_ips"],
        }

    return ret


def list_nodes_min(call=None):
    """
    Return a list of the instances that are on the provider. Only a list of
    instances names, and their state, is returned.

    CLI Examples:

    .. code-block:: bash

        salt-cloud -f list_nodes_min my-qingcloud
    """
    if call != "function":
        raise SaltCloudSystemExit(
            "The list_nodes_min function must be called with -f or --function."
        )

    nodes = list_nodes_full()

    result = {}
    for instance_id, full_node in nodes.items():
        result[instance_id] = {
            "name": full_node["instance_name"],
            "status": full_node["status"],
        }

    return result


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

    CLI Examples:

    .. code-block:: bash

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


def show_instance(instance_id, call=None, kwargs=None):
    """
    Show the details from QingCloud concerning an instance.

    CLI Examples:

    .. code-block:: bash

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

    params = {
        "action": "DescribeInstances",
        "instances.1": instance_id,
        "zone": _get_specified_zone(kwargs=None, provider=get_configured_provider()),
    }
    items = query(params=params)

    if items["total_count"] == 0:
        raise SaltCloudNotFound(
            f"The specified instance, '{instance_id}', could not be found."
        )

    full_node = items["instance_set"][0]
    normalized_node = _show_normalized_node(full_node)
    full_node.update(normalized_node)

    result = full_node

    return result


def _query_node_data(instance_id):
    data = show_instance(instance_id, call="action")

    if not data:
        return False

    if data.get("private_ips", []):
        return data


def create(vm_):
    """
    Create a single instance from a data dict.

    CLI Examples:

    .. code-block:: bash

        salt-cloud -p qingcloud-ubuntu-c1m1 hostname1
        salt-cloud -m /path/to/mymap.sls -P
    """
    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 "qingcloud",
                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"])

    # params
    params = {
        "action": "RunInstances",
        "instance_name": vm_["name"],
        "zone": _get_location(vm_),
        "instance_type": _get_size(vm_),
        "image_id": _get_image(vm_),
        "vxnets.1": vm_["vxnets"],
        "login_mode": vm_["login_mode"],
        "login_keypair": vm_["login_keypair"],
    }

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

    result = query(params)
    new_instance_id = result["instances"][0]

    try:
        data = salt.utils.cloud.wait_for_ip(
            _query_node_data,
            update_args=(new_instance_id,),
            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))

    private_ip = data["private_ips"][0]

    log.debug("VM %s is now running", private_ip)

    vm_["ssh_host"] = private_ip

    # The instance is booted and accessible, let's Salt it!
    __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 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 start(instance_id, call=None):
    """
    Start an instance.

    CLI Examples:

    .. code-block:: bash

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

    log.info("Starting instance %s", instance_id)

    params = {
        "action": "StartInstances",
        "zone": _get_specified_zone(provider=get_configured_provider()),
        "instances.1": instance_id,
    }
    result = query(params)

    return result


def stop(instance_id, force=False, call=None):
    """
    Stop an instance.

    CLI Examples:

    .. code-block:: bash

        salt-cloud -a stop i-2f733r5n
        salt-cloud -a stop i-2f733r5n force=True
    """
    if call != "action":
        raise SaltCloudSystemExit("The stop action must be called with -a or --action.")

    log.info("Stopping instance %s", instance_id)

    params = {
        "action": "StopInstances",
        "zone": _get_specified_zone(provider=get_configured_provider()),
        "instances.1": instance_id,
        "force": int(force),
    }
    result = query(params)

    return result


def reboot(instance_id, call=None):
    """
    Reboot an instance.

    CLI Examples:

    .. code-block:: bash

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

    log.info("Rebooting instance %s", instance_id)

    params = {
        "action": "RestartInstances",
        "zone": _get_specified_zone(provider=get_configured_provider()),
        "instances.1": instance_id,
    }
    result = query(params)

    return result


def destroy(instance_id, call=None):
    """
    Destroy an instance.

    CLI Example:

    .. code-block:: bash

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

    instance_data = show_instance(instance_id, call="action")
    name = instance_data["instance_name"]

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

    params = {
        "action": "TerminateInstances",
        "zone": _get_specified_zone(provider=get_configured_provider()),
        "instances.1": instance_id,
    }
    result = query(params)

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

    return result
