# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json

from libcloud.utils.py3 import httplib
from libcloud.utils.networking import is_private_subnet

from libcloud.common.onapp import OnAppConnection
from libcloud.compute.base import Node, NodeDriver, NodeImage, KeyPair
from libcloud.compute.providers import Provider


__all__ = [
    "OnAppNodeDriver"
]

"""
Define the extra dictionary for specific resources
"""
RESOURCE_EXTRA_ATTRIBUTES_MAP = {
    "node": {
        "add_to_marketplace": {
            "key_name": "add_to_marketplace",
            "transform_func": bool
        },
        "admin_note": {
            "key_name": "admin_note",
            "transform_func": str
        },
        "allow_resize_without_reboot": {
            "key_name": "allow_resize_without_reboot",
            "transform_func": bool
        },
        "allowed_hot_migrate": {
            "key_name": "allowed_hot_migrate",
            "transform_func": bool
        },
        "allowed_swap": {
            "key_name": "allowed_swap",
            "transform_func": bool
        },
        "booted": {
            "key_name": "booted",
            "transform_func": bool
        },
        "built": {
            "key_name": "built",
            "transform_func": bool
        },
        "cpu_priority": {
            "key_name": "cpu_priority",
            "transform_func": int
        },
        "cpu_shares": {
            "key_name": "cpu_shares",
            "transform_func": int
        },
        "cpu_sockets": {
            "key_name": "cpu_sockets",
            "transform_func": int
        },
        "cpu_threads": {
            "key_name": "cpu_threads",
            "transform_func": int
        },
        "cpu_units": {
            "key_name": "cpu_units",
            "transform_func": int
        },
        "cpus": {
            "key_name": "cpus",
            "transform_func": int
        },
        "created_at": {
            "key_name": "created_at",
            "transform_func": str
        },
        "customer_network_id": {
            "key_name": "customer_network_id",
            "transform_func": str
        },
        "deleted_at": {
            "key_name": "deleted_at",
            "transform_func": str
        },
        "edge_server_type": {
            "key_name": "edge_server_type",
            "transform_func": str
        },
        "enable_autoscale": {
            "key_name": "enable_autoscale",
            "transform_func": bool
        },
        "enable_monitis": {
            "key_name": "enable_monitis",
            "transform_func": bool
        },
        "firewall_notrack": {
            "key_name": "firewall_notrack",
            "transform_func": bool
        },
        "hostname": {
            "key_name": "hostname",
            "transform_func": str
        },
        "hypervisor_id": {
            "key_name": "hypervisor_id",
            "transform_func": int
        },
        "id": {
            "key_name": "id",
            "transform_func": int
        },
        "initial_root_password": {
            "key_name": "initial_root_password",
            "transform_func": str
        },
        "initial_root_password_encrypted": {
            "key_name": "initial_root_password_encrypted",
            "transform_func": bool
        },
        "local_remote_access_ip_address": {
            "key_name": "local_remote_access_ip_address",
            "transform_func": str
        },
        "local_remote_access_port": {
            "key_name": "local_remote_access_port",
            "transform_func": int
        },
        "locked": {
            "key_name": "locked",
            "transform_func": bool
        },
        "memory": {
            "key_name": "memory",
            "transform_func": int
        },
        "min_disk_size": {
            "key_name": "min_disk_size",
            "transform_func": int
        },
        "monthly_bandwidth_used": {
            "key_name": "monthly_bandwidth_used",
            "transform_func": int
        },
        "note": {
            "key_name": "note",
            "transform_func": str
        },
        "operating_system": {
            "key_name": "operating_system",
            "transform_func": str
        },
        "operating_system_distro": {
            "key_name": "operating_system_distro",
            "transform_func": str
        },
        "preferred_hvs": {
            "key_name": "preferred_hvs",
            "transform_func": list
        },
        "price_per_hour": {
            "key_name": "price_per_hour",
            "transform_func": float
        },
        "price_per_hour_powered_off": {
            "key_name": "price_per_hour_powered_off",
            "transform_func": float
        },
        "recovery_mode": {
            "key_name": "recovery_mode",
            "transform_func": bool
        },
        "remote_access_password": {
            "key_name": "remote_access_password",
            "transform_func": str
        },
        "service_password": {
            "key_name": "service_password",
            "transform_func": str
        },
        "state": {
            "key_name": "state",
            "transform_func": str
        },
        "storage_server_type": {
            "key_name": "storage_server_type",
            "transform_func": str
        },
        "strict_virtual_machine_id": {
            "key_name": "strict_virtual_machine_id",
            "transform_func": str
        },
        "support_incremental_backups": {
            "key_name": "support_incremental_backups",
            "transform_func": bool
        },
        "suspended": {
            "key_name": "suspended",
            "transform_func": bool
        },
        "template_id": {
            "key_name": "template_id",
            "transform_func": int
        },
        "template_label": {
            "key_name": "template_label",
            "transform_func": str
        },
        "total_disk_size": {
            "key_name": "total_disk_size",
            "transform_func": int
        },
        "updated_at": {
            "key_name": "updated_at",
            "transform_func": str
        },
        "user_id": {
            "key_name": "user_id",
            "transform_func": int
        },
        "vip": {
            "key_name": "vip",
            "transform_func": bool
        },
        "xen_id": {
            "key_name": "xen_id",
            "transform_func": int
        }
    }
}


class OnAppNodeDriver(NodeDriver):
    """
    Base OnApp node driver.
    """

    connectionCls = OnAppConnection
    type = Provider.ONAPP
    name = 'OnApp'
    website = 'http://onapp.com/'

    def create_node(self, name, ex_memory, ex_cpus, ex_cpu_shares,
                    ex_hostname, ex_template_id, ex_primary_disk_size,
                    ex_swap_disk_size, ex_required_virtual_machine_build=1,
                    ex_required_ip_address_assignment=1, **kwargs):
        """
        Add a VS

        :param  kwargs: All keyword arguments to create a VS
        :type   kwargs: ``dict``

        :rtype: :class:`OnAppNode`
        """
        server_params = dict(
            label=name,
            memory=ex_memory,
            cpus=ex_cpus,
            cpu_shares=ex_cpu_shares,
            hostname=ex_hostname,
            template_id=ex_template_id,
            primary_disk_size=ex_primary_disk_size,
            swap_disk_size=ex_swap_disk_size,
            required_virtual_machine_build=ex_required_virtual_machine_build,
            required_ip_address_assignment=ex_required_ip_address_assignment,
            rate_limit=kwargs.get("rate_limit")
        )

        server_params.update(OnAppNodeDriver._create_args_to_params(**kwargs))
        data = json.dumps({"virtual_machine": server_params})

        response = self.connection.request(
            "/virtual_machines.json",
            data=data,
            headers={
                "Content-type": "application/json"},
            method="POST")

        return self._to_node(response.object["virtual_machine"])

    def destroy_node(self,
                     node,
                     ex_convert_last_backup=0,
                     ex_destroy_all_backups=0):
        """
        Delete a VS

        :param node: OnApp node
        :type  node: :class: `OnAppNode`

        :param convert_last_backup: set 1 to convert the last VS's backup to
                                    template, otherwise set 0
        :type  convert_last_backup: ``int``

        :param destroy_all_backups: set 1 to destroy all existing backups of
                                    this VS, otherwise set 0
        :type  destroy_all_backups: ``int``
        """
        server_params = {
            "convert_last_backup": ex_convert_last_backup,
            "destroy_all_backups": ex_destroy_all_backups
        }
        action = "/virtual_machines/{identifier}.json".format(
            identifier=node.id)

        self.connection.request(action, params=server_params, method="DELETE")
        return True

    def list_nodes(self):
        """
        List all VS

        :rtype: ``list`` of :class:`OnAppNode`
        """
        response = self.connection.request("/virtual_machines.json")
        nodes = []
        for vm in response.object:
            nodes.append(self._to_node(vm["virtual_machine"]))
        return nodes

    def list_images(self):
        """
        List all images

        :rtype: ``list`` of :class:`NodeImage`
        """
        response = self.connection.request("/templates.json")
        templates = []
        for template in response.object:
            templates.append(self._to_image(template["image_template"]))
        return templates

    def list_key_pairs(self):
        """
        List all the available key pair objects.

        :rtype: ``list`` of :class:`.KeyPair` objects
        """
        user_id = self.connection.request('/profile.json').object['user']['id']
        response = self.connection.request('/users/%s/ssh_keys.json' % user_id)
        ssh_keys = []
        for ssh_key in response.object:
            ssh_keys.append(self._to_key_pair(ssh_key['ssh_key']))
        return ssh_keys

    def get_key_pair(self, name):
        """
        Retrieve a single key pair.

        :param name: ID of the key pair to retrieve.
        :type name: ``str``

        :rtype: :class:`.KeyPair` object
        """
        user_id = self.connection.request('/profile.json').object['user']['id']
        response = self.connection.request(
            '/users/%s/ssh_keys/%s.json' % (user_id, name))
        return self._to_key_pair(response.object['ssh_key'])

    def import_key_pair_from_string(self, name, key_material):
        """
        Import a new public key from string.

        :param name: Key pair name (unused).
        :type name: ``str``

        :param key_material: Public key material.
        :type key_material: ``str``

        :rtype: :class:`.KeyPair` object
        """
        data = json.dumps({'key': key_material})
        user_id = self.connection.request('/profile.json').object['user']['id']
        response = self.connection.request(
            '/users/%s/ssh_keys.json' % user_id,
            data=data,
            headers={
                "Content-type": "application/json"},
            method="POST")
        return self._to_key_pair(response.object['ssh_key'])

    def delete_key_pair(self, key):
        """
        Delete an existing key pair.

        :param key_pair: Key pair object.
        :type key_pair: :class:`.KeyPair`

        :return: True on success
        :rtype: ``bool``
        """
        key_id = key.name
        response = self.connection.request(
            '/settings/ssh_keys/%s.json' % key_id,
            method='DELETE')
        return response.status == httplib.NO_CONTENT

    #
    # Helper methods
    #

    def _to_key_pair(self, data):
        extra = {'created_at': data['created_at'],
                 'updated_at': data['updated_at']}
        return KeyPair(name=data['id'],
                       fingerprint=None,
                       public_key=data['key'],
                       private_key=None,
                       driver=self,
                       extra=extra)

    def _to_image(self, template):
        extra = {'distribution': template['operating_system_distro'],
                 'operating_system': template['operating_system'],
                 'operating_system_arch': template['operating_system_arch'],
                 'allow_resize_without_reboot':
                 template['allow_resize_without_reboot'],
                 'allowed_hot_migrate': template['allowed_hot_migrate'],
                 'allowed_swap': template['allowed_swap'],
                 'min_disk_size': template['min_disk_size'],
                 'min_memory_size': template['min_memory_size'],
                 'created_at': template['created_at']}
        return NodeImage(id=template['id'], name=template['label'],
                         driver=self, extra=extra)

    def _to_node(self, data):
        identifier = data["identifier"]
        name = data["label"]
        private_ips = []
        public_ips = []
        for ip in data["ip_addresses"]:
            address = ip["ip_address"]['address']
            if is_private_subnet(address):
                private_ips.append(address)
            else:
                public_ips.append(address)

        extra = OnAppNodeDriver._get_extra_dict(
            data, RESOURCE_EXTRA_ATTRIBUTES_MAP["node"]
        )
        return Node(identifier,
                    name,
                    extra['state'],
                    public_ips,
                    private_ips,
                    self,
                    extra=extra)

    @staticmethod
    def _get_extra_dict(response, mapping):
        """
        Extract attributes from the element based on rules provided in the
        mapping dictionary.

        :param   response: The JSON response to parse the values from.
        :type    response: ``dict``

        :param   mapping: Dictionary with the extra layout
        :type    mapping: ``dict``

        :rtype:  ``dict``
        """
        extra = {}
        for attribute, values in mapping.items():
            transform_func = values["transform_func"]
            value = response.get(values["key_name"])

            extra[attribute] = transform_func(value) if value else None
        return extra

    @staticmethod
    def _create_args_to_params(**kwargs):
        """
        Extract server params from keyword args to create a VS

        :param   kwargs: keyword args
        :return: ``dict``
        """
        params = [
            "ex_cpu_sockets",
            "ex_cpu_threads",
            "ex_enable_autoscale",
            "ex_data_store_group_primary_id",
            "ex_data_store_group_swap_id",
            "ex_hypervisor_group_id",
            "ex_hypervisor_id",
            "ex_initial_root_password",
            "ex_note",
            "ex_primary_disk_min_iops",
            "ex_primary_network_id",
            "ex_primary_network_group_id",
            "ex_recipe_ids",
            "ex_required_automatic_backup",
            "ex_required_virtual_machine_startup",
            "ex_required_virtual_machine_startup",
            "ex_selected_ip_address_id",
            "ex_swap_disk_min_iops",
            "ex_type_of_format",
            "ex_custom_recipe_variables",
            "ex_licensing_key",
            "ex_licensing_server_id",
            "ex_licensing_type",
        ]
        server_params = {}

        for p in params:
            value = kwargs.get(p)
            if value:
                server_params[p[3:]] = value
        return server_params
