"""
Packet Cloud Module Using Packet's Python API Client
====================================================

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

Use of this module only requires the ``token`` parameter.

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

The Packet profile requires ``size``, ``image``, ``location``,  ``project_id``

Optional profile parameters:

- ``storage_size`` -  min value is 10, defines Gigabytes of storage that will be attached to device.
- ``storage_tier`` - storage_1 - Standard Plan, storage_2 - Performance Plan
- ``snapshot_count`` - int
- ``snapshot_frequency`` - string - possible values:

  - 1min
  - 15min
  - 1hour
  - 1day
  - 1week
  - 1month
  - 1year

This driver requires Packet's client library: https://pypi.python.org/pypi/packet-python

.. code-block:: yaml

    packet-provider:
        minion:
            master: 192.168.50.10
        driver: packet
        token: ewr23rdf35wC8oNjJrhmHa87rjSXzJyi
        private_key: /root/.ssh/id_rsa

    packet-profile:
        provider: packet-provider
        size: baremetal_0
        image: ubuntu_16_04_image
        location: ewr1
        project_id: a64d000b-d47c-4d26-9870-46aac43010a6
        storage_size: 10
        storage_tier: storage_1
        storage_snapshot_count: 1
        storage_snapshot_frequency: 15min
"""

import logging
import pprint
import time

import salt.config as config
import salt.utils.cloud
from salt.cloud.libcloudfuncs import get_image, get_size, script, show_instance
from salt.exceptions import SaltCloudException, SaltCloudSystemExit
from salt.utils.functools import namespaced_function

try:
    import packet

    HAS_PACKET = True
except ImportError:
    HAS_PACKET = False


get_size = namespaced_function(get_size, globals())
get_image = namespaced_function(get_image, globals())

script = namespaced_function(script, globals())

show_instance = namespaced_function(show_instance, globals())

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

__virtualname__ = "packet"


# Only load this module if the Packet configuration is in place.
def __virtual__():
    """
    Check for Packet configs.
    """
    if HAS_PACKET is False:
        return False, "The packet python library is not installed"
    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__, ("token",)
    )


def avail_images(call=None):
    """
    Return available Packet os images.

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-images packet-provider
        salt-cloud -f avail_images packet-provider
    """
    if call == "action":
        raise SaltCloudException(
            "The avail_images function must be called with -f or --function."
        )

    ret = {}

    vm_ = get_configured_provider()
    manager = packet.Manager(auth_token=vm_["token"])

    ret = {}

    for os_system in manager.list_operating_systems():
        ret[os_system.name] = os_system.__dict__

    return ret


def avail_locations(call=None):
    """
    Return available Packet datacenter locations.

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-locations packet-provider
        salt-cloud -f avail_locations packet-provider
    """
    if call == "action":
        raise SaltCloudException(
            "The avail_locations function must be called with -f or --function."
        )

    vm_ = get_configured_provider()
    manager = packet.Manager(auth_token=vm_["token"])

    ret = {}

    for facility in manager.list_facilities():
        ret[facility.name] = facility.__dict__

    return ret


def avail_sizes(call=None):
    """
    Return available Packet sizes.

    CLI Example:

    .. code-block:: bash

        salt-cloud --list-sizes packet-provider
        salt-cloud -f avail_sizes packet-provider
    """
    if call == "action":
        raise SaltCloudException(
            "The avail_locations function must be called with -f or --function."
        )

    vm_ = get_configured_provider()

    manager = packet.Manager(auth_token=vm_["token"])

    ret = {}

    for plan in manager.list_plans():
        ret[plan.name] = plan.__dict__

    return ret


def avail_projects(call=None):
    """
    Return available Packet projects.

    CLI Example:

    .. code-block:: bash

        salt-cloud -f avail_projects packet-provider
    """
    if call == "action":
        raise SaltCloudException(
            "The avail_projects function must be called with -f or --function."
        )

    vm_ = get_configured_provider()
    manager = packet.Manager(auth_token=vm_["token"])

    ret = {}

    for project in manager.list_projects():
        ret[project.name] = project.__dict__

    return ret


def _wait_for_status(status_type, object_id, status=None, timeout=500, quiet=True):
    """
    Wait for a certain status from Packet.
    status_type
        device or volume
    object_id
        The ID of the Packet device or volume to wait on. Required.
    status
        The status to wait for.
    timeout
        The amount of time to wait for a status to update.
    quiet
        Log status updates to debug logs when False. Otherwise, logs to info.
    """
    if status is None:
        status = "ok"

    interval = 5
    iterations = int(timeout / interval)

    vm_ = get_configured_provider()
    manager = packet.Manager(auth_token=vm_["token"])

    for i in range(0, iterations):
        get_object = getattr(manager, f"get_{status_type}")
        obj = get_object(object_id)

        if obj.state == status:
            return obj

        time.sleep(interval)
        log.log(
            logging.INFO if not quiet else logging.DEBUG,
            "Status for Packet %s is '%s', waiting for '%s'.",
            object_id,
            obj.state,
            status,
        )

    return obj


def is_profile_configured(vm_):
    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 __virtualname__,
                vm_["profile"],
                vm_=vm_,
            )
            is False
        ):
            return False

        alias, driver = _get_active_provider_name().split(":")

        profile_data = __opts__["providers"][alias][driver]["profiles"][vm_["profile"]]

        if profile_data.get("storage_size") or profile_data.get("storage_tier"):
            required_keys = ["storage_size", "storage_tier"]

            for key in required_keys:
                if profile_data.get(key) is None:
                    log.error(
                        "both storage_size and storage_tier required for "
                        "profile %s. Please check your profile configuration",
                        vm_["profile"],
                    )
                    return False

            locations = avail_locations()

            for location in locations.values():
                if location["code"] == profile_data["location"]:
                    if "storage" not in location["features"]:
                        log.error(
                            "Chosen location %s for profile %s does not "
                            "support storage feature. Please check your "
                            "profile configuration",
                            location["code"],
                            vm_["profile"],
                        )
                        return False

        if profile_data.get("storage_snapshot_count") or profile_data.get(
            "storage_snapshot_frequency"
        ):
            required_keys = ["storage_size", "storage_tier"]

            for key in required_keys:
                if profile_data.get(key) is None:
                    log.error(
                        "both storage_snapshot_count and "
                        "storage_snapshot_frequency required for profile "
                        "%s. Please check your profile configuration",
                        vm_["profile"],
                    )
                    return False

    except AttributeError:
        pass

    return True


def create(vm_):
    """
    Create a single Packet VM.
    """
    name = vm_["name"]

    if not is_profile_configured(vm_):
        return False

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

    log.info("Creating Packet VM %s", name)

    manager = packet.Manager(auth_token=vm_["token"])

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

    device = manager.create_device(
        project_id=vm_["project_id"],
        hostname=name,
        plan=vm_["size"],
        facility=vm_["location"],
        operating_system=vm_["image"],
    )

    device = _wait_for_status("device", device.id, status="active")

    if device.state != "active":
        log.error(
            "Error creating %s on PACKET\n\nwhile waiting for initial ready status",
            name,
            exc_info_on_loglevel=logging.DEBUG,
        )

    # Define which ssh_interface to use
    ssh_interface = _get_ssh_interface(vm_)

    # Pass the correct IP address to the bootstrap ssh_host key
    if ssh_interface == "private_ips":
        for ip in device.ip_addresses:
            if ip["public"] is False:
                vm_["ssh_host"] = ip["address"]
                break
    else:
        for ip in device.ip_addresses:
            if ip["public"] is True:
                vm_["ssh_host"] = ip["address"]
                break

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

    vm_["key_filename"] = key_filename

    vm_["private_key"] = key_filename

    # Bootstrap!
    ret = __utils__["cloud.bootstrap"](vm_, __opts__)

    ret.update({"device": device.__dict__})

    if vm_.get("storage_tier") and vm_.get("storage_size"):
        # create storage and attach it to device

        volume = manager.create_volume(
            vm_["project_id"],
            f"{name}_storage",
            vm_.get("storage_tier"),
            vm_.get("storage_size"),
            vm_.get("location"),
            snapshot_count=vm_.get("storage_snapshot_count", 0),
            snapshot_frequency=vm_.get("storage_snapshot_frequency"),
        )

        volume.attach(device.id)

        volume = _wait_for_status("volume", volume.id, status="active")

        if volume.state != "active":
            log.error(
                "Error creating %s on PACKET\n\nwhile waiting for initial ready status",
                name,
                exc_info_on_loglevel=logging.DEBUG,
            )

        ret.update({"volume": volume.__dict__})

    log.info("Created Cloud VM '%s'", name)

    log.debug("'%s' VM creation details:\n%s", name, pprint.pformat(device.__dict__))

    __utils__["cloud.fire_event"](
        "event",
        "created instance",
        f"salt/cloud/{name}/created",
        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(call=None):
    """
    List devices, with all available information.

    CLI Example:

    .. code-block:: bash

        salt-cloud -F
        salt-cloud --full-query
        salt-cloud -f list_nodes_full packet-provider

    ..
    """
    if call == "action":
        raise SaltCloudException(
            "The list_nodes_full function must be called with -f or --function."
        )

    ret = {}

    for device in get_devices_by_token():
        ret[device.hostname] = device.__dict__

    return ret


def list_nodes_min(call=None):
    """
    Return a list of the VMs that are on the provider. Only a list of VM names and
    their state is returned. This is the minimum amount of information needed to
    check for existing VMs.

    .. versionadded:: 2015.8.0

    CLI Example:

    .. code-block:: bash

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

    ret = {}

    for device in get_devices_by_token():
        ret[device.hostname] = {"id": device.id, "state": device.state}

    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_devices_by_token():
    vm_ = get_configured_provider()
    manager = packet.Manager(auth_token=vm_["token"])

    devices = []

    for profile_name in vm_["profiles"]:
        profile = vm_["profiles"][profile_name]

        devices.extend(manager.list_devices(profile["project_id"]))

    return devices


def list_nodes(call=None):
    """
    Returns a list of devices, keeping only a brief listing.

    CLI Example:

    .. code-block:: bash

        salt-cloud -Q
        salt-cloud --query
        salt-cloud -f list_nodes packet-provider
    ..
    """

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

    ret = {}

    for device in get_devices_by_token():
        ret[device.hostname] = device.__dict__

    return ret


def destroy(name, call=None):
    """
    Destroys a Packet device by name.

    name
        The hostname of VM to be be destroyed.

    CLI Example:

    .. code-block:: bash

        salt-cloud -d name
    """
    if call == "function":
        raise SaltCloudException(
            "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"],
    )

    vm_ = get_configured_provider()
    manager = packet.Manager(auth_token=vm_["token"])

    nodes = list_nodes_min()

    node = nodes[name]

    for project in manager.list_projects():

        for volume in manager.list_volumes(project.id):
            if volume.attached_to == node["id"]:
                volume.detach()
                volume.delete()
                break

    manager.call_api("devices/{id}".format(id=node["id"]), type="DELETE")

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

    return {}


def _get_ssh_interface(vm_):
    """
    Return the ssh_interface type to connect to. Either 'public_ips' (default)
    or 'private_ips'.
    """
    return config.get_cloud_config_value(
        "ssh_interface", vm_, __opts__, default="public_ips", search_global=False
    )
