# 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
import time

from libcloud.common.exceptions import BaseHTTPError
from libcloud.common.types import LibcloudError


class UpcloudTimeoutException(LibcloudError):
    pass


class UpcloudCreateNodeRequestBody(object):
    """
    Body of the create_node request

    Takes the create_node arguments (**kwargs) and constructs the request body

    :param      name: Name of the created server (required)
    :type       name: ``str``

    :param      size: The size of resources allocated to this node.
    :type       size: :class:`.NodeSize`

    :param      image: OS Image to boot on node.
    :type       image: :class:`.NodeImage`

    :param      location: Which data center to create a node in. If empty,
                        undefined behavior will be selected. (optional)
    :type       location: :class:`.NodeLocation`

    :param      auth: Initial authentication information for the node
                            (optional)
    :type       auth: :class:`.NodeAuthSSHKey`

    :param      ex_hostname: Hostname. Default is 'localhost'. (optional)
    :type       ex_hostname: ``str``

    :param ex_username: User's username, which is created.
                        Default is 'root'. (optional)
    :type ex_username: ``str``
    """

    def __init__(self, name, size, image, location, auth=None,
                 **kwargs):
        username = kwargs.get('ex_username', 'root')
        self.body = {
            'server': {
                'title': name,
                'hostname': kwargs.get('ex_hostname', 'localhost'),
                'plan': size.id,
                'zone': location.id,
                'login_user': _LoginUser(username, auth).to_dict(),
                'storage_devices': _StorageDevice(image, size).to_dict()
            }
        }

    def to_json(self):
        """
        Serializes the body to json

        :return: JSON string
        :rtype: ``str``
        """
        return json.dumps(self.body)


class UpcloudNodeDestroyer(object):
    """
    Helper class for destroying node.
    Node must be first stopped and then it can be
    destroyed

    :param  upcloud_node_operations: UpcloudNodeOperations instance
    :type   upcloud_node_operations: :class:`.UpcloudNodeOperations`

    :param  sleep_func: Callable function, which sleeps.
        Takes int argument to sleep in seconds (optional)
    :type   sleep_func: ``function``

    """

    WAIT_AMOUNT = 2
    SLEEP_COUNT_TO_TIMEOUT = 20

    def __init__(self, upcloud_node_operations, sleep_func=None):
        self._operations = upcloud_node_operations
        self._sleep_func = sleep_func or time.sleep
        self._sleep_count = 0

    def destroy_node(self, node_id):
        """
        Destroys the given node.

        :param  node_id: Id of the Node.
        :type   node_id: ``int``
        """
        self._stop_called = False
        self._sleep_count = 0
        return self._do_destroy_node(node_id)

    def _do_destroy_node(self, node_id):
        state = self._operations.get_node_state(node_id)
        if state == 'stopped':
            self._operations.destroy_node(node_id)
            return True
        elif state == 'error':
            return False
        elif state == 'started':
            if not self._stop_called:
                self._operations.stop_node(node_id)
                self._stop_called = True
            else:
                # Waiting for started state to change and
                # not calling stop again
                self._sleep()
            return self._do_destroy_node(node_id)
        elif state == 'maintenance':
            # Lets wait maintenace state to go away and retry destroy
            self._sleep()
            return self._do_destroy_node(node_id)
        elif state is None:  # Server not found any more
            return True

    def _sleep(self):
        if self._sleep_count > self.SLEEP_COUNT_TO_TIMEOUT:
            raise UpcloudTimeoutException("Timeout, could not destroy node")
        self._sleep_count += 1
        self._sleep_func(self.WAIT_AMOUNT)


class UpcloudNodeOperations(object):
    """
    Helper class to start and stop node.

    :param  conneciton: Connection instance
    :type   connection: :class:`.UpcloudConnection`
    """

    def __init__(self, connection):
        self.connection = connection

    def stop_node(self, node_id):
        """
        Stops the node

        :param  node_id: Id of the Node
        :type   node_id: ``int``
        """
        body = {
            'stop_server': {
                'stop_type': 'hard'
            }
        }
        self.connection.request('1.2/server/{0}/stop'.format(node_id),
                                method='POST',
                                data=json.dumps(body))

    def get_node_state(self, node_id):
        """
        Get the state of the node.

        :param  node_id: Id of the Node
        :type   node_id: ``int``

        :rtype: ``str``
        """

        action = '1.2/server/{0}'.format(node_id)
        try:
            response = self.connection.request(action)
            return response.object['server']['state']
        except BaseHTTPError as e:
            if e.code == 404:
                return None
            raise

    def destroy_node(self, node_id):
        """
        Destroys the node.

        :param  node_id: Id of the Node
        :type   node_id: ``int``
        """
        self.connection.request('1.2/server/{0}'.format(node_id),
                                method='DELETE')


class PlanPrice(object):
    """
    Helper class to construct plan price in different zones

    :param  zone_prices: List of prices in different zones in UpCloud
    :type   zone_prices: ```list```

    """

    def __init__(self, zone_prices):
        self._zone_prices = zone_prices

    def get_price(self, plan_name, location=None):
        """
        Returns the plan's price in location. If location
        is not provided returns None

        :param  plan_name: Name of the plan
        :type   plan_name: ```str```

        :param  location: Location, which price is returned (optional)
        :type   location: :class:`.NodeLocation`


        rtype: ``float``
        """
        if location is None:
            return None
        server_plan_name = 'server_plan_' + plan_name

        for zone_price in self._zone_prices:
            if zone_price['name'] == location.id:
                return zone_price.get(server_plan_name, {}).get('price')
        return None


class _LoginUser(object):

    def __init__(self, user_id, auth=None):
        self.user_id = user_id
        self.auth = auth

    def to_dict(self):
        login_user = {'username': self.user_id}
        if self.auth is not None:
            login_user['ssh_keys'] = {
                'ssh_key': [self.auth.pubkey]
            }
        else:
            login_user['create_password'] = 'yes'

        return login_user


class _StorageDevice(object):

    def __init__(self, image, size):
        self.image = image
        self.size = size

    def to_dict(self):
        extra = self.image.extra
        if extra['type'] == 'template':
            return self._storage_device_for_template_image()
        elif extra['type'] == 'cdrom':
            return self._storage_device_for_cdrom_image()

    def _storage_device_for_template_image(self):
        hdd_device = {
            'action': 'clone',
            'storage': self.image.id
        }
        hdd_device.update(self._common_hdd_device())
        return {'storage_device': [hdd_device]}

    def _storage_device_for_cdrom_image(self):
        hdd_device = {'action': 'create'}
        hdd_device.update(self._common_hdd_device())
        storage_devices = {
            'storage_device': [
                hdd_device,
                {
                    'action': 'attach',
                    'storage': self.image.id,
                    'type': 'cdrom'
                }
            ]
        }
        return storage_devices

    def _common_hdd_device(self):
        return {
            'title': self.image.name,
            'size': self.size.disk,
            'tier': self.size.extra.get('storage_tier', 'maxiops')
        }
