# 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.

"""
libcloud driver for the Host Virtual Inc. (VR) API
Home page https://www.hostvirtual.com/
"""

import re
import time

from libcloud.compute.base import (
    Node,
    NodeSize,
    NodeImage,
    NodeDriver,
    NodeLocation,
    NodeAuthSSHKey,
    NodeAuthPassword,
)
from libcloud.compute.types import NodeState
from libcloud.compute.providers import Provider
from libcloud.common.hostvirtual import (
    HostVirtualResponse,
    HostVirtualException,
    HostVirtualConnection,
)

try:
    import simplejson as json
except ImportError:
    import json


API_ROOT = ""

NODE_STATE_MAP = {
    "BUILDING": NodeState.PENDING,
    "PENDING": NodeState.PENDING,
    "RUNNING": NodeState.RUNNING,  # server is powered up
    "STOPPING": NodeState.REBOOTING,
    "REBOOTING": NodeState.REBOOTING,
    "STARTING": NodeState.REBOOTING,
    "TERMINATED": NodeState.TERMINATED,  # server is powered down
    "STOPPED": NodeState.STOPPED,
}

DEFAULT_NODE_LOCATION_ID = 21


class HostVirtualComputeResponse(HostVirtualResponse):
    pass


class HostVirtualComputeConnection(HostVirtualConnection):
    responseCls = HostVirtualComputeResponse


class HostVirtualNodeDriver(NodeDriver):
    type = Provider.HOSTVIRTUAL
    name = "HostVirtual"
    website = "http://www.hostvirtual.com"
    connectionCls = HostVirtualComputeConnection
    features = {"create_node": ["ssh_key", "password"]}

    def __init__(self, key, secure=True, host=None, port=None):
        self.location = None
        super().__init__(key=key, secure=secure, host=host, port=port)

    def list_nodes(self):
        try:
            result = self.connection.request(API_ROOT + "/cloud/servers/").object
        except HostVirtualException:
            return []
        nodes = []
        for value in result:
            node = self._to_node(value)
            nodes.append(node)
        return nodes

    def list_locations(self):
        result = self.connection.request(API_ROOT + "/cloud/locations/").object
        locations = []
        for k in result:
            dc = result[k]
            locations.append(
                NodeLocation(
                    dc["id"],
                    dc["name"],
                    dc["name"].split(",")[1].replace(" ", ""),  # country
                    self,
                )
            )
        return sorted(locations, key=lambda x: int(x.id))

    def list_sizes(self, location=None):
        params = {}
        if location is not None:
            params = {"location": location.id}
        result = self.connection.request(API_ROOT + "/cloud/sizes/", params=params).object
        sizes = []
        for size in result:
            n = NodeSize(
                id=size["plan_id"],
                name=size["plan"],
                ram=size["ram"],
                disk=size["disk"],
                bandwidth=size["transfer"],
                price=size["price"],
                driver=self.connection.driver,
            )
            sizes.append(n)
        return sizes

    def list_images(self):
        result = self.connection.request(API_ROOT + "/cloud/images/").object
        images = []
        for image in result:
            i = NodeImage(
                id=image["id"],
                name=image["os"],
                driver=self.connection.driver,
                extra=image,
            )
            del i.extra["id"]
            del i.extra["os"]
            images.append(i)
        return images

    def create_node(self, name, image, size, location=None, auth=None):
        """
        Creates a node

        Example of node creation with ssh key deployed:

        >>> from libcloud.compute.base import NodeAuthSSHKey
        >>> key = open('/home/user/.ssh/id_rsa.pub').read()
        >>> auth = NodeAuthSSHKey(pubkey=key)
        >>> from libcloud.compute.providers import get_driver
        >>> driver = get_driver('hostvirtual')
        >>> conn = driver('API_KEY')
        >>> image = conn.list_images()[1]
        >>> size = conn.list_sizes()[0]
        >>> location = conn.list_locations()[1]
        >>> name = 'markos-dev'
        >>> node = conn.create_node(name, image, size, auth=auth,
        >>>                         location=location)
        """

        dc = None

        auth = self._get_and_check_auth(auth)

        if not self._is_valid_fqdn(name):
            raise HostVirtualException(
                500, "Name should be a valid FQDN (e.g, hostname.example.com)"
            )

        # simply order a package first
        pkg = self.ex_order_package(size)

        if location:
            dc = location.id
        else:
            dc = DEFAULT_NODE_LOCATION_ID

        # create a stub node
        stub_node = self._to_node(
            {
                "mbpkgid": pkg["id"],
                "status": "PENDING",
                "fqdn": name,
                "plan_id": size.id,
                "os_id": image.id,
                "location_id": dc,
            }
        )

        # provisioning a server using the stub node
        self.ex_provision_node(node=stub_node, auth=auth)
        node = self._wait_for_node(stub_node.id)
        if getattr(auth, "generated", False):
            node.extra["password"] = auth.password

        return node

    def reboot_node(self, node):
        params = {"force": 0, "mbpkgid": node.id}
        result = self.connection.request(
            API_ROOT + "/cloud/server/reboot", data=json.dumps(params), method="POST"
        ).object

        return bool(result)

    def destroy_node(self, node):
        params = {
            "mbpkgid": node.id,
            # 'reason': 'Submitted through Libcloud API'
        }

        result = self.connection.request(
            API_ROOT + "/cloud/cancel", data=json.dumps(params), method="POST"
        ).object

        return bool(result)

    def ex_list_packages(self):
        """
        List the server packages.

        """

        try:
            result = self.connection.request(API_ROOT + "/cloud/packages/").object
        except HostVirtualException:
            return []
        pkgs = []
        for value in result:
            pkgs.append(value)
        return pkgs

    def ex_order_package(self, size):
        """
        Order a server package.

        :param      size:
        :type       node: :class:`NodeSize`

        :rtype: ``str``
        """

        params = {"plan": size.name}
        pkg = self.connection.request(
            API_ROOT + "/cloud/buy/", data=json.dumps(params), method="POST"
        ).object

        return pkg

    def ex_cancel_package(self, node):
        """
        Cancel a server package.

        :param      node: Node which should be used
        :type       node: :class:`Node`

        :rtype: ``str``
        """

        params = {"mbpkgid": node.id}
        result = self.connection.request(
            API_ROOT + "/cloud/cancel/", data=json.dumps(params), method="POST"
        ).object

        return result

    def ex_unlink_package(self, node):
        """
        Unlink a server package from location.

        :param      node: Node which should be used
        :type       node: :class:`Node`

        :rtype: ``str``
        """

        params = {"mbpkgid": node.id}
        result = self.connection.request(
            API_ROOT + "/cloud/unlink/", data=json.dumps(params), method="POST"
        ).object

        return result

    def ex_get_node(self, node_id):
        """
        Get a single node.

        :param      node_id: id of the node that we need the node object for
        :type       node_id: ``str``

        :rtype: :class:`Node`
        """

        params = {"mbpkgid": node_id}
        result = self.connection.request(API_ROOT + "/cloud/server", params=params).object
        node = self._to_node(result)
        return node

    def start_node(self, node):
        """
        Start a node.

        :param      node: Node which should be used
        :type       node: :class:`Node`

        :rtype: ``bool``
        """
        params = {"mbpkgid": node.id}
        result = self.connection.request(
            API_ROOT + "/cloud/server/start", data=json.dumps(params), method="POST"
        ).object

        return bool(result)

    def stop_node(self, node):
        """
        Stop a node.

        :param      node: Node which should be used
        :type       node: :class:`Node`

        :rtype: ``bool``
        """
        params = {"force": 0, "mbpkgid": node.id}
        result = self.connection.request(
            API_ROOT + "/cloud/server/shutdown", data=json.dumps(params), method="POST"
        ).object

        return bool(result)

    def ex_start_node(self, node):
        # NOTE: This method is here for backward compatibility reasons after
        # this method was promoted to be part of the standard compute API in
        # Libcloud v2.7.0
        return self.start_node(node=node)

    def ex_stop_node(self, node):
        # NOTE: This method is here for backward compatibility reasons after
        # this method was promoted to be part of the standard compute API in
        # Libcloud v2.7.0
        return self.stop_node(node=node)

    def ex_provision_node(self, **kwargs):
        """
        Provision a server on a VR package and get it booted

        :keyword node: node which should be used
        :type    node: :class:`Node`

        :keyword image: The distribution to deploy on your server (mandatory)
        :type    image: :class:`NodeImage`

        :keyword auth: an SSH key or root password (mandatory)
        :type    auth: :class:`NodeAuthSSHKey` or :class:`NodeAuthPassword`

        :keyword location: which datacenter to create the server in
        :type    location: :class:`NodeLocation`

        :return: Node representing the newly built server
        :rtype: :class:`Node`
        """

        node = kwargs["node"]

        if "image" in kwargs:
            image = kwargs["image"]
        else:
            image = node.extra["image"]

        params = {
            "mbpkgid": node.id,
            "image": image,
            "fqdn": node.name,
            "location": node.extra["location"],
        }

        auth = kwargs["auth"]

        ssh_key = None
        password = None
        if isinstance(auth, NodeAuthSSHKey):
            ssh_key = auth.pubkey
            params["ssh_key"] = ssh_key
        elif isinstance(auth, NodeAuthPassword):
            password = auth.password
            params["password"] = password

        if not ssh_key and not password:
            raise HostVirtualException(500, "SSH key or Root password is required")

        try:
            result = self.connection.request(
                API_ROOT + "/cloud/server/build", data=json.dumps(params), method="POST"
            ).object
            return bool(result)
        except HostVirtualException:
            self.ex_cancel_package(node)

    def ex_delete_node(self, node):
        """
        Delete a node.

        :param      node: Node which should be used
        :type       node: :class:`Node`

        :rtype: ``bool``
        """

        params = {"mbpkgid": node.id}
        result = self.connection.request(
            API_ROOT + "/cloud/server/delete", data=json.dumps(params), method="POST"
        ).object

        return bool(result)

    def _to_node(self, data):
        state = NODE_STATE_MAP[data["status"]]
        public_ips = []
        private_ips = []
        extra = {}

        if "plan_id" in data:
            extra["size"] = data["plan_id"]
        if "os_id" in data:
            extra["image"] = data["os_id"]
        if "fqdn" in data:
            extra["fqdn"] = data["fqdn"]
        if "location_id" in data:
            extra["location"] = data["location_id"]
        if "ip" in data:
            public_ips.append(data["ip"])

        node = Node(
            id=data["mbpkgid"],
            name=data["fqdn"],
            state=state,
            public_ips=public_ips,
            private_ips=private_ips,
            driver=self.connection.driver,
            extra=extra,
        )
        return node

    def _wait_for_node(self, node_id, timeout=30, interval=5.0):
        """
        :param node_id: ID of the node to wait for.
        :type node_id: ``int``

        :param timeout: Timeout (in seconds).
        :type timeout: ``int``

        :param interval: How long to wait (in seconds) between each attempt.
        :type interval: ``float``

        :return: Node representing the newly built server
        :rtype: :class:`Node`
        """
        # poll until we get a node
        for i in range(0, timeout, int(interval)):
            try:
                node = self.ex_get_node(node_id)
                return node
            except HostVirtualException:
                time.sleep(interval)

        raise HostVirtualException(412, "Timeout on getting node details")

    def _is_valid_fqdn(self, fqdn):
        if len(fqdn) > 255:
            return False
        if fqdn[-1] == ".":
            fqdn = fqdn[:-1]
        valid = re.compile(r"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
        if len(fqdn.split(".")) > 1:
            return all(valid.match(x) for x in fqdn.split("."))
        else:
            return False
