#
#    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 collections
import logging
import re

from dracclient import constants
from dracclient import exceptions
from dracclient.resources import lifecycle_controller
from dracclient.resources import uris
from dracclient import utils

LOG = logging.getLogger(__name__)

POWER_STATES = {
    '2': constants.POWER_ON,
    '3': constants.POWER_OFF,
    '11': constants.REBOOT,
}

REVERSE_POWER_STATES = dict((v, k) for (k, v) in POWER_STATES.items())

BOOT_MODE_IS_CURRENT = {
    '1': True,
    '2': False
}

BOOT_MODE_IS_NEXT = {
    '1': True,   # is next
    '2': False,  # is not next
    '3': True    # is next for single use (one time boot only)
}

LC_CONTROLLER_VERSION_12G = (2, 0, 0)

BootMode = collections.namedtuple('BootMode', ['id', 'name', 'is_current',
                                               'is_next'])

BootDevice = collections.namedtuple(
    'BootDevice',
    ['id',  'boot_mode', 'current_assigned_sequence',
     'pending_assigned_sequence', 'bios_boot_string'])


class PowerManagement(object):

    def __init__(self, client):
        """Creates PowerManagement object

        :param client: an instance of WSManClient
        """
        self.client = client

    def get_power_state(self):
        """Returns the current power state of the node

        :returns: power state of the node, one of 'POWER_ON', 'POWER_OFF' or
                  'REBOOT'
        :raises: WSManRequestFailure on request failures
        :raises: WSManInvalidResponse when receiving invalid response
        :raises: DRACOperationFailed on error reported back by the DRAC
                 interface
        """

        filter_query = ('select EnabledState from DCIM_ComputerSystem')
        doc = self.client.enumerate(uris.DCIM_ComputerSystem,
                                    filter_query=filter_query)
        enabled_state = utils.find_xml(doc, 'EnabledState',
                                       uris.DCIM_ComputerSystem)

        return POWER_STATES[enabled_state.text]

    def set_power_state(self, target_state):
        """Turns the server power on/off or do a reboot

        :param target_state: target power state. Valid options are: 'POWER_ON',
                             'POWER_OFF' and 'REBOOT'.
        :raises: WSManRequestFailure on request failures
        :raises: WSManInvalidResponse when receiving invalid response
        :raises: DRACOperationFailed on error reported back by the DRAC
                 interface
        :raises: DRACUnexpectedReturnValue on return value mismatch
        :raises: InvalidParameterValue on invalid target power state
        """

        try:
            drac_requested_state = REVERSE_POWER_STATES[target_state]
        except KeyError:
            msg = ("'%(target_state)s' is not supported. "
                   "Supported power states: %(supported_power_states)r") % {
                       'target_state': target_state,
                       'supported_power_states': list(REVERSE_POWER_STATES)}
            raise exceptions.InvalidParameterValue(reason=msg)

        selectors = {'CreationClassName': 'DCIM_ComputerSystem',
                     'Name': 'srv:system'}
        properties = {'RequestedState': drac_requested_state}

        self.client.invoke(uris.DCIM_ComputerSystem, 'RequestStateChange',
                           selectors, properties)


class BootManagement(object):

    def __init__(self, client):
        """Creates BootManagement object

        :param client: an instance of WSManClient
        """
        self.client = client

    def list_boot_modes(self):
        """Returns the list of boot modes

        :returns: list of BootMode objects
        :raises: WSManRequestFailure on request failures
        :raises: WSManInvalidResponse when receiving invalid response
        :raises: DRACOperationFailed on error reported back by the DRAC
                 interface
        """

        doc = self.client.enumerate(uris.DCIM_BootConfigSetting)

        drac_boot_modes = utils.find_xml(doc, 'DCIM_BootConfigSetting',
                                         uris.DCIM_BootConfigSetting,
                                         find_all=True)

        return [self._parse_drac_boot_mode(drac_boot_mode)
                for drac_boot_mode in drac_boot_modes]

    def list_boot_devices(self):
        """Returns the list of boot devices

        :returns: a dictionary with the boot modes and the list of associated
                  BootDevice objects, ordered by the pending_assigned_sequence
                  property
        :raises: WSManRequestFailure on request failures
        :raises: WSManInvalidResponse when receiving invalid response
        :raises: DRACOperationFailed on error reported back by the DRAC
                 interface
        """

        doc = self.client.enumerate(uris.DCIM_BootSourceSetting)

        drac_boot_devices = utils.find_xml(doc, 'DCIM_BootSourceSetting',
                                           uris.DCIM_BootSourceSetting,
                                           find_all=True)
        try:
            boot_devices = [self._parse_drac_boot_device(drac_boot_device)
                            for drac_boot_device in drac_boot_devices]
        except AttributeError:
            # DRAC 11g doesn't have the BootSourceType attribute on the
            # DCIM_BootSourceSetting resource
            controller_version = (
                lifecycle_controller.LifecycleControllerManagement(
                    self.client).get_version())

            if controller_version < LC_CONTROLLER_VERSION_12G:
                boot_devices = [
                    self._parse_drac_boot_device_11g(drac_boot_device)
                    for drac_boot_device in drac_boot_devices]
            else:
                raise

        # group devices by boot mode
        boot_devices_per_mode = {device.boot_mode: []
                                 for device in boot_devices}
        for device in boot_devices:
            boot_devices_per_mode[device.boot_mode].append(device)

        # sort the device list by pending assigned seqeuence
        for mode in boot_devices_per_mode.keys():
            boot_devices_per_mode[mode].sort(
                key=lambda device: device.pending_assigned_sequence)

        return boot_devices_per_mode

    def change_boot_device_order(self, boot_mode, boot_device_list):
        """Changes the boot device sequence for a boot mode

        :param boot_mode: boot mode for which the boot device list is to be
                          changed
        :param boot_device_list: a list of boot device ids in an order
                                 representing the desired boot sequence
        :raises: WSManRequestFailure on request failures
        :raises: WSManInvalidResponse when receiving invalid response
        :raises: DRACOperationFailed on error reported back by the DRAC
                 interface
        :raises: DRACUnexpectedReturnValue on return value mismatch
        """

        selectors = {'InstanceID': boot_mode}
        properties = {'source': boot_device_list}

        self.client.invoke(uris.DCIM_BootConfigSetting,
                           'ChangeBootOrderByInstanceID', selectors,
                           properties, expected_return_value=utils.RET_SUCCESS)

    def _parse_drac_boot_mode(self, drac_boot_mode):
        return BootMode(
            id=self._get_boot_mode_attr(drac_boot_mode, 'InstanceID'),
            name=self._get_boot_mode_attr(drac_boot_mode, 'ElementName'),
            is_current=BOOT_MODE_IS_CURRENT[self._get_boot_mode_attr(
                drac_boot_mode, 'IsCurrent')],
            is_next=BOOT_MODE_IS_NEXT[self._get_boot_mode_attr(
                drac_boot_mode, 'IsNext')])

    def _get_boot_mode_attr(self, drac_boot_mode, attr_name):
        return utils.get_wsman_resource_attr(drac_boot_mode,
                                             uris.DCIM_BootConfigSetting,
                                             attr_name)

    def _parse_drac_boot_device_common(self, drac_boot_device, instance_id,
                                       boot_mode):
        return BootDevice(
            id=instance_id,
            boot_mode=boot_mode,
            current_assigned_sequence=int(self._get_boot_device_attr(
                drac_boot_device, 'CurrentAssignedSequence')),
            pending_assigned_sequence=int(self._get_boot_device_attr(
                drac_boot_device, 'PendingAssignedSequence')),
            bios_boot_string=self._get_boot_device_attr(drac_boot_device,
                                                        'BIOSBootString'))

    def _parse_drac_boot_device(self, drac_boot_device):
        instance_id = self._get_boot_device_attr(drac_boot_device,
                                                 'InstanceID')
        boot_mode = self._get_boot_device_attr(drac_boot_device,
                                               'BootSourceType')

        return self._parse_drac_boot_device_common(drac_boot_device,
                                                   instance_id, boot_mode)

    def _parse_drac_boot_device_11g(self, drac_boot_device):
        instance_id = self._get_boot_device_attr(drac_boot_device,
                                                 'InstanceID')
        boot_mode = instance_id.split(':')[0]

        return self._parse_drac_boot_device_common(drac_boot_device,
                                                   instance_id, boot_mode)

    def _get_boot_device_attr(self, drac_boot_device, attr_name):
        return utils.get_wsman_resource_attr(drac_boot_device,
                                             uris.DCIM_BootSourceSetting,
                                             attr_name)


class BIOSAttribute(object):
    """Generic BIOS attribute class"""

    def __init__(self, name, instance_id, current_value, pending_value,
                 read_only):
        """Creates BIOSAttribute object

        :param name: name of the BIOS attribute
        :param instance_id: opaque and unique identifier of the BIOS attribute
        :param current_value: current value of the BIOS attribute
        :param pending_value: pending value of the BIOS attribute, reflecting
                an unprocessed change (eg. config job not completed)
        :param read_only: indicates whether this BIOS attribute can be changed
        """
        self.name = name
        self.instance_id = instance_id
        self.current_value = current_value
        self.pending_value = pending_value
        self.read_only = read_only

    def __eq__(self, other):
        return self.__dict__ == other.__dict__

    def __ne__(self, other):
        return not self.__eq__(other)

    @classmethod
    def parse(cls, namespace, bios_attr_xml):
        """Parses XML and creates BIOSAttribute object"""

        name = utils.get_wsman_resource_attr(
            bios_attr_xml, namespace, 'AttributeName')
        instance_id = utils.get_wsman_resource_attr(
            bios_attr_xml, namespace, 'InstanceID')
        current_value = utils.get_wsman_resource_attr(
            bios_attr_xml, namespace, 'CurrentValue', nullable=True)
        pending_value = utils.get_wsman_resource_attr(
            bios_attr_xml, namespace, 'PendingValue', nullable=True)
        read_only = utils.get_wsman_resource_attr(
            bios_attr_xml, namespace, 'IsReadOnly')

        return cls(name, instance_id, current_value, pending_value,
                   (read_only == 'true'))


class BIOSEnumerableAttribute(BIOSAttribute):
    """Enumerable BIOS attribute class"""

    namespace = uris.DCIM_BIOSEnumeration

    def __init__(self, name, instance_id, current_value, pending_value,
                 read_only, possible_values):
        """Creates BIOSEnumerableAttribute object

        :param name: name of the BIOS attribute
        :param current_value: current value of the BIOS attribute
        :param pending_value: pending value of the BIOS attribute, reflecting
                an unprocessed change (eg. config job not completed)
        :param read_only: indicates whether this BIOS attribute can be changed
        :param possible_values: list containing the allowed values for the BIOS
                                attribute
        """
        super(BIOSEnumerableAttribute, self).__init__(name, instance_id,
                                                      current_value,
                                                      pending_value, read_only)
        self.possible_values = possible_values

    @classmethod
    def parse(cls, bios_attr_xml):
        """Parses XML and creates BIOSEnumerableAttribute object"""

        bios_attr = BIOSAttribute.parse(cls.namespace, bios_attr_xml)
        possible_values = [attr.text for attr
                           in utils.find_xml(bios_attr_xml, 'PossibleValues',
                                             cls.namespace, find_all=True)]

        return cls(bios_attr.name, bios_attr.instance_id,
                   bios_attr.current_value, bios_attr.pending_value,
                   bios_attr.read_only, possible_values)

    def validate(self, new_value):
        """Validates new value"""

        if str(new_value) not in self.possible_values:
            msg = ("Attribute '%(attr)s' cannot be set to value '%(val)s'."
                   " It must be in %(possible_values)r.") % {
                       'attr': self.name,
                       'val': new_value,
                       'possible_values': self.possible_values}
            return msg


class BIOSStringAttribute(BIOSAttribute):
    """String BIOS attribute class"""

    namespace = uris.DCIM_BIOSString

    def __init__(self, name, instance_id, current_value, pending_value,
                 read_only, min_length, max_length, pcre_regex):
        """Creates BIOSStringAttribute object

        :param name: name of the BIOS attribute
        :param current_value: current value of the BIOS attribute
        :param pending_value: pending value of the BIOS attribute, reflecting
                an unprocessed change (eg. config job not completed)
        :param read_only: indicates whether this BIOS attribute can be changed
        :param min_length: minimum length of the string
        :param max_length: maximum length of the string
        :param pcre_regex: is a PCRE compatible regular expression that the
                           string must match
        """
        super(BIOSStringAttribute, self).__init__(name, instance_id,
                                                  current_value, pending_value,
                                                  read_only)
        self.min_length = min_length
        self.max_length = max_length
        self.pcre_regex = pcre_regex

    @classmethod
    def parse(cls, bios_attr_xml):
        """Parses XML and creates BIOSStringAttribute object"""

        bios_attr = BIOSAttribute.parse(cls.namespace, bios_attr_xml)
        min_length = int(utils.get_wsman_resource_attr(
            bios_attr_xml, cls.namespace, 'MinLength'))
        max_length = int(utils.get_wsman_resource_attr(
            bios_attr_xml, cls.namespace, 'MaxLength'))
        pcre_regex = utils.get_wsman_resource_attr(
            bios_attr_xml, cls.namespace, 'ValueExpression', nullable=True)

        return cls(bios_attr.name, bios_attr.instance_id,
                   bios_attr.current_value, bios_attr.pending_value,
                   bios_attr.read_only, min_length, max_length, pcre_regex)

    def validate(self, new_value):
        """Validates new value"""

        if self.pcre_regex is not None:
            regex = re.compile(self.pcre_regex)
            if regex.search(str(new_value)) is None:
                msg = ("Attribute '%(attr)s' cannot be set to value '%(val)s.'"
                       " It must match regex '%(re)s'.") % {
                           'attr': self.name,
                           'val': new_value,
                           're': self.pcre_regex}
                return msg


class BIOSIntegerAttribute(BIOSAttribute):
    """Integer BIOS attribute class"""

    namespace = uris.DCIM_BIOSInteger

    def __init__(self, name, instance_id, current_value, pending_value,
                 read_only, lower_bound, upper_bound):
        """Creates BIOSIntegerAttribute object

        :param name: name of the BIOS attribute
        :param current_value: current value of the BIOS attribute
        :param pending_value: pending value of the BIOS attribute, reflecting
                an unprocessed change (eg. config job not completed)
        :param read_only: indicates whether this BIOS attribute can be changed
        :param lower_bound: minimum value for the BIOS attribute
        :param upper_bound: maximum value for the BIOS attribute
        """
        super(BIOSIntegerAttribute, self).__init__(name, instance_id,
                                                   current_value,
                                                   pending_value, read_only)
        self.lower_bound = lower_bound
        self.upper_bound = upper_bound

    @classmethod
    def parse(cls, bios_attr_xml):
        """Parses XML and creates BIOSIntegerAttribute object"""

        bios_attr = BIOSAttribute.parse(cls.namespace, bios_attr_xml)
        lower_bound = utils.get_wsman_resource_attr(
            bios_attr_xml, cls.namespace, 'LowerBound')
        upper_bound = utils.get_wsman_resource_attr(
            bios_attr_xml, cls.namespace, 'UpperBound')

        if bios_attr.current_value:
            bios_attr.current_value = int(bios_attr.current_value)
        if bios_attr.pending_value:
            bios_attr.pending_value = int(bios_attr.pending_value)

        return cls(bios_attr.name, bios_attr.instance_id,
                   bios_attr.current_value, bios_attr.pending_value,
                   bios_attr.read_only, int(lower_bound), int(upper_bound))

    def validate(self, new_value):
        """Validates new value"""

        val = int(new_value)
        if val < self.lower_bound or val > self.upper_bound:
            msg = ('Attribute %(attr)s cannot be set to value %(val)d.'
                   ' It must be between %(lower)d and %(upper)d.') % {
                       'attr': self.name,
                       'val': new_value,
                       'lower': self.lower_bound,
                       'upper': self.upper_bound}
            return msg


class BIOSConfiguration(object):

    NAMESPACES = [(uris.DCIM_BIOSEnumeration, BIOSEnumerableAttribute),
                  (uris.DCIM_BIOSString, BIOSStringAttribute),
                  (uris.DCIM_BIOSInteger, BIOSIntegerAttribute)]

    def __init__(self, client):
        """Creates BIOSConfiguration object

        :param client: an instance of WSManClient
        """
        self.client = client

    def list_bios_settings(self, by_name=True):
        """List the BIOS configuration settings

        :param by_name: Controls whether returned dictionary uses BIOS
                        attribute name or instance_id as key.
        :returns: a dictionary with the BIOS settings using its name as the
                  key. The attributes are either BIOSEnumerableAttribute,
                  BIOSStringAttribute or BIOSIntegerAttribute objects.
        :raises: WSManRequestFailure on request failures
        :raises: WSManInvalidResponse when receiving invalid response
        :raises: DRACOperationFailed on error reported back by the DRAC
                 interface
        """

        return utils.list_settings(self.client, self.NAMESPACES, by_name)

    def set_bios_settings(self, new_settings):
        """Sets the BIOS configuration

        To be more precise, it sets the pending_value parameter for each of the
        attributes passed in. For the values to be applied, a config job must
        be created and the node must be rebooted.

        :param new_settings: a dictionary containing the proposed values, with
                             each key being the name of attribute and the
                             value being the proposed value.
        :returns: a dictionary containing:
                 - The is_commit_required key with a boolean value indicating
                   whether a config job must be created for the values to be
                   applied.
                 - The is_reboot_required key with a RebootRequired enumerated
                   value indicating whether the server must be rebooted for the
                   values to be applied.  Possible values are true and false.
        :raises: WSManRequestFailure on request failures
        :raises: WSManInvalidResponse when receiving invalid response
        :raises: DRACOperationFailed on error reported back by the DRAC
                 interface
        :raises: DRACUnexpectedReturnValue on return value mismatch
        :raises: InvalidParameterValue on invalid BIOS attribute
        """

        return utils.set_settings('BIOS',
                                  self.client,
                                  self.NAMESPACES,
                                  new_settings,
                                  uris.DCIM_BIOSService,
                                  "DCIM_BIOSService",
                                  "DCIM:BIOSService",
                                  'BIOS.Setup.1-1')
