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

from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from horizon.utils import memoized
from ironic_inspector_client import client as inspector_client
from ironicclient import client as ironic_client
from openstack_dashboard.api import base
from openstack_dashboard.api import glance
from openstack_dashboard.api import nova

from tuskar_ui.cached_property import cached_property  # noqa
from tuskar_ui.handle_errors import handle_errors  # noqa
from tuskar_ui.utils import utils


# power states
ERROR_STATES = set(['deploy failed', 'error'])
POWER_ON_STATES = set(['on', 'power on'])

# provision_states of ironic aggregated to reasonable groups
PROVISION_STATE_FREE = ['available', 'deleted', None]
PROVISION_STATE_PROVISIONED = ['active']
PROVISION_STATE_PROVISIONING = [
    'deploying', 'wait call-back', 'rebuild', 'deploy complete']
PROVISION_STATE_DELETING = ['deleting']
PROVISION_STATE_ERROR = ['error', 'deploy failed']

# names for states of ironic used in UI,
# provison_states + discovery states
DISCOVERING_STATE = 'discovering'
DISCOVERED_STATE = 'discovered'
DISCOVERY_FAILED_STATE = 'discovery failed'
MAINTENANCE_STATE = 'manageable'
PROVISIONED_STATE = 'provisioned'
PROVISIONING_FAILED_STATE = 'provisioning failed'
PROVISIONING_STATE = 'provisioning'
DELETING_STATE = 'deleting'
FREE_STATE = 'free'


IRONIC_DISCOVERD_URL = getattr(settings, 'IRONIC_DISCOVERD_URL', None)
LOG = logging.getLogger(__name__)


@memoized.memoized
def ironicclient(request):
    api_version = 1
    kwargs = {'os_auth_token': request.user.token.id,
              'ironic_url': base.url_for(request, 'baremetal')}
    return ironic_client.get_client(api_version, **kwargs)


# FIXME(lsmola) This should be done in Horizon, they don't have caching
@memoized.memoized
@handle_errors(_("Unable to retrieve image."))
def image_get(request, image_id):
    """Returns an Image object with metadata

    Returns an Image object populated with metadata for image
    with supplied identifier.

    :param image_id: list of objects to be put into a dict
    :type  image_id: list

    :return: object
    :rtype: glanceclient.v1.images.Image
    """
    image = glance.image_get(request, image_id)
    return image


class Node(base.APIResourceWrapper):
    _attrs = ('id', 'uuid', 'instance_uuid', 'driver', 'driver_info',
              'properties', 'power_state', 'target_power_state',
              'provision_state', 'maintenance', 'extra')

    def __init__(self, apiresource, request=None, instance=None):
        """Initialize a Node

        :param apiresource: apiresource we want to wrap
        :type  apiresource: IronicNode

        :param request: request
        :type  request: django.core.handlers.wsgi.WSGIRequest

        :param instance: instance relation we want to cache
        :type  instance: openstack_dashboard.api.nova.Server

        :return: Node object
        :rtype:  tusar_ui.api.node.Node
        """
        super(Node, self).__init__(apiresource)
        self._request = request
        self._instance = instance

    @classmethod
    def create(cls, request, ipmi_address=None, cpu_arch=None, cpus=None,
               memory_mb=None, local_gb=None, mac_addresses=[],
               ipmi_username=None, ipmi_password=None, ssh_address=None,
               ssh_username=None, ssh_key_contents=None,
               deployment_kernel=None, deployment_ramdisk=None,
               driver=None):
        """Create a Node in Ironic."""
        if driver == 'pxe_ssh':
            driver_info = {
                'ssh_address': ssh_address,
                'ssh_username': ssh_username,
                'ssh_key_contents': ssh_key_contents,
                'ssh_virt_type': 'virsh',
            }
        else:
            driver_info = {
                'ipmi_address': ipmi_address,
                'ipmi_username': ipmi_username,
                'ipmi_password': ipmi_password
            }
        driver_info.update(
            deploy_kernel=deployment_kernel,
            deploy_ramdisk=deployment_ramdisk
        )

        properties = {'capabilities': 'boot_option:local', }
        if cpus:
            properties.update(cpus=cpus)
        if memory_mb:
            properties.update(memory_mb=memory_mb)
        if local_gb:
            properties.update(local_gb=local_gb)
        if cpu_arch:
            properties.update(cpu_arch=cpu_arch)

        node = ironicclient(request).node.create(
            driver=driver,
            driver_info=driver_info,
            properties=properties,
        )
        for mac_address in mac_addresses:
            ironicclient(request).port.create(
                node_uuid=node.uuid,
                address=mac_address
            )

        return cls(node, request)

    @classmethod
    @memoized.memoized
    @handle_errors(_("Unable to retrieve node"))
    def get(cls, request, uuid):
        """Return the Node that matches the ID

        :param request: request object
        :type  request: django.http.HttpRequest

        :param uuid: ID of Node to be retrieved
        :type  uuid: str

        :return: matching Node, or None if no IronicNode matches the ID
        :rtype:  tuskar_ui.api.node.Node
        """
        node = ironicclient(request).node.get(uuid)
        if node.instance_uuid is not None:
            server = nova.server_get(request, node.instance_uuid)
        else:
            server = None
        return cls(node, request, server)

    @classmethod
    @handle_errors(_("Unable to retrieve node"))
    def get_by_instance_uuid(cls, request, instance_uuid):
        """Return the Node associated with the instance ID

        :param request: request object
        :type  request: django.http.HttpRequest

        :param instance_uuid: ID of Instance that is deployed on the Node
                              to be retrieved
        :type  instance_uuid: str

        :return: matching Node
        :rtype:  tuskar_ui.api.node.Node

        :raises: ironicclient.exc.HTTPNotFound if there is no Node with
                 the matching instance UUID
        """
        node = ironicclient(request).node.get_by_instance_uuid(instance_uuid)
        server = nova.server_get(request, instance_uuid)
        return cls(node, request, server)

    @classmethod
    @memoized.memoized
    @handle_errors(_("Unable to retrieve nodes"), [])
    def list(cls, request, associated=None, maintenance=None):
        """Return a list of Nodes

        :param request: request object
        :type  request: django.http.HttpRequest

        :param associated: should we also retrieve all Nodes, only those
                           associated with an Instance, or only those not
                           associated with an Instance?
        :type  associated: bool

        :param maintenance: should we also retrieve all Nodes, only those
                            in maintenance mode, or those which are not in
                            maintenance mode?
        :type  maintenance: bool

        :return: list of Nodes, or an empty list if there are none
        :rtype:  list of tuskar_ui.api.node.Node
        """
        nodes = ironicclient(request).node.list(associated=associated,
                                                maintenance=maintenance)
        if associated is None or associated:
            servers = nova.server_list(request)[0]
            servers_dict = utils.list_to_dict(servers)
            nodes_with_instance = []
            for n in nodes:
                server = servers_dict.get(n.instance_uuid, None)
                nodes_with_instance.append(cls(n, instance=server,
                                               request=request))
            return [cls.get(request, node.uuid)
                    for node in nodes_with_instance]
        return [cls.get(request, node.uuid) for node in nodes]

    @classmethod
    def delete(cls, request, uuid):
        """Delete an Node

        Remove the IronicNode matching the ID if it
        exists; otherwise, does nothing.

        :param request: request object
        :type  request: django.http.HttpRequest

        :param uuid: ID of IronicNode to be removed
        :type  uuid: str
        """
        return ironicclient(request).node.delete(uuid)

    @classmethod
    def discover(cls, request, uuids):
        """Set the maintenance status of node

        :param request: request object
        :type  request: django.http.HttpRequest

        :param uuids: IDs of IronicNodes
        :type  uuids: list of str
        """
        if not IRONIC_DISCOVERD_URL:
            return
        for uuid in uuids:

            inspector_client.introspect(
                uuid,
                base_url=IRONIC_DISCOVERD_URL,
                auth_token=request.user.token.id)

            # NOTE(dtantsur): PXE firmware on virtual machines misbehaves when
            # a lot of nodes start DHCPing simultaneously: it ignores NACK from
            # DHCP server, tries to get the same address, then times out. Work
            # around it by using sleep, anyway introspection takes much longer.
            time.sleep(5)

    @classmethod
    def set_maintenance(cls, request, uuid, maintenance):
        """Set the maintenance status of node

        :param request: request object
        :type  request: django.http.HttpRequest

        :param uuid: ID of Node to be removed
        :type  uuid: str

        :param maintenance: desired maintenance state
        :type  maintenance: bool
        """
        patch = {
            'op': 'replace',
            'value': 'True' if maintenance else 'False',
            'path': '/maintenance'
        }
        node = ironicclient(request).node.update(uuid, [patch])
        return cls(node, request)

    @classmethod
    def set_power_state(cls, request, uuid, power_state):
        """Set the power_state of node

        :param request: request object
        :type  request: django.http.HttpRequest

        :param uuid: ID of Node
        :type  uuid: str

        :param power_state: desired power_state
        :type  power_state: str
        """
        node = ironicclient(request).node.set_power_state(uuid, power_state)
        return cls(node, request)

    @classmethod
    @memoized.memoized
    def list_ports(cls, request, uuid):
        """Return a list of ports associated with this Node

        :param request: request object
        :type  request: django.http.HttpRequest

        :param uuid: ID of IronicNode
        :type  uuid: str
        """
        return ironicclient(request).node.list_ports(uuid)

    @cached_property
    def addresses(self):
        """Return a list of port addresses associated with this IronicNode

        :return: list of port addresses associated with this IronicNode, or
                 an empty list if no addresses are associated with
                 this IronicNode
        :rtype:  list of str
        """
        ports = self.list_ports(self._request, self.uuid)
        return [port.address for port in ports]

    @cached_property
    def cpus(self):
        return self.properties.get('cpus', None)

    @cached_property
    def memory_mb(self):
        return self.properties.get('memory_mb', None)

    @cached_property
    def local_gb(self):
        return self.properties.get('local_gb', None)

    @cached_property
    def cpu_arch(self):
        return self.properties.get('cpu_arch', None)

    @cached_property
    def state(self):
        if self.maintenance:
            if not IRONIC_DISCOVERD_URL:
                return MAINTENANCE_STATE
            try:
                status = inspector_client.get_status(
                    uuid=self.uuid,
                    base_url=IRONIC_DISCOVERD_URL,
                    auth_token=self._request.user.token.id,
                )
            except inspector_client.ClientError as e:
                if getattr(e.response, 'status_code', None) == 404:
                    return MAINTENANCE_STATE
                raise
            if status['error']:
                return DISCOVERY_FAILED_STATE
            elif status['finished']:
                return DISCOVERED_STATE
            else:
                return DISCOVERING_STATE
        else:
            if self.provision_state in PROVISION_STATE_FREE:
                return FREE_STATE
            if self.provision_state in PROVISION_STATE_PROVISIONING:
                return PROVISIONING_STATE
            if self.provision_state in PROVISION_STATE_PROVISIONED:
                return PROVISIONED_STATE
            if self.provision_state in PROVISION_STATE_DELETING:
                return DELETING_STATE
            if self.provision_state in PROVISION_STATE_ERROR:
                return PROVISIONING_FAILED_STATE
        # Unknown state
        return None

    @cached_property
    def instance(self):
        """Return the Nova Instance associated with this Node

        :return: Nova Instance associated with this Node; or
                 None if there is no Instance associated with this
                 Node, or no matching Instance is found
        :rtype:  Instance
        """
        if self._instance is not None:
            return self._instance
        if self.instance_uuid:
            servers, _has_more_data = nova.server_list(self._request)
            for server in servers:
                if server.id == self.instance_uuid:
                    return server

    @cached_property
    def ip_address(self):
        try:
            apiresource = self.instace._apiresource
        except AttributeError:
            LOG.error("Couldn't obtain IP address")
            return None
        return apiresource.addresses['ctlplane'][0]['addr']

    @cached_property
    def image_name(self):
        """Return image name of associated instance

        Returns image name of instance associated with node

        :return: Image name of instance
        :rtype:  string
        """
        if self.instance is None:
            return
        image = image_get(self._request, self.instance.image['id'])
        return image.name

    @cached_property
    def instance_status(self):
        return getattr(getattr(self, 'instance', None), 'status', None)

    @cached_property
    def provisioning_status(self):
        if self.instance_uuid:
            return _("Provisioned")
        return _("Free")

    @classmethod
    def get_all_mac_addresses(cls, request):
        macs = [node.addresses for node in cls.list(request)]
        return set([mac.upper() for sublist in macs for mac in sublist])
