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

"""
AMT Management Driver
"""
import copy

from ironic.common import exception as ironic_exception
from ironic.conductor import task_manager
from ironic.drivers import base
from oslo_log import log as logging
from oslo_utils import excutils
from oslo_utils import importutils

from ironic_staging_drivers.amt import common as amt_common
from ironic_staging_drivers.amt import resource_uris
from ironic_staging_drivers.common import exception
from ironic_staging_drivers.common.i18n import _
from ironic_staging_drivers.common.i18n import _LE
from ironic_staging_drivers.common.i18n import _LI

pywsman = importutils.try_import('pywsman')

LOG = logging.getLogger(__name__)


_ADDRESS = 'http://schemas.xmlsoap.org/ws/2004/08/addressing'
_ANONYMOUS = 'http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous'
_WSMAN = 'http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd'


def _generate_change_boot_order_input(device):
    """Generate Xmldoc as change_boot_order input.

    This generates a Xmldoc used as input for change_boot_order.

    :param device: the boot device.
    :returns: Xmldoc.
    """
    method_input = "ChangeBootOrder_INPUT"
    namespace = resource_uris.CIM_BootConfigSetting
    doc = pywsman.XmlDoc(method_input)
    root = doc.root()
    root.set_ns(namespace)

    child = root.add(namespace, 'Source', None)
    child.add(_ADDRESS, 'Address', _ANONYMOUS)

    grand_child = child.add(_ADDRESS, 'ReferenceParameters', None)
    grand_child.add(_WSMAN, 'ResourceURI', resource_uris.CIM_BootSourceSetting)
    g_grand_child = grand_child.add(_WSMAN, 'SelectorSet', None)
    g_g_grand_child = g_grand_child.add(_WSMAN, 'Selector', device)
    g_g_grand_child.attr_add(_WSMAN, 'Name', 'InstanceID')
    return doc


def _set_boot_device_order(node, boot_device):
    """Set boot device order configuration of AMT Client.

    :param node: a node object
    :param boot_device: the boot device
    :raises: AMTFailure
    :raises: AMTConnectFailure
    """
    amt_common.awake_amt_interface(node)
    client = amt_common.get_wsman_client(node)
    device = amt_common.BOOT_DEVICES_MAPPING[boot_device]
    doc = _generate_change_boot_order_input(device)

    method = 'ChangeBootOrder'

    options = pywsman.ClientOptions()
    options.add_selector('InstanceID', 'Intel(r) AMT: Boot Configuration 0')

    try:
        client.wsman_invoke(options, resource_uris.CIM_BootConfigSetting,
                            method, doc)
    except (exception.AMTFailure, exception.AMTConnectFailure) as e:
        with excutils.save_and_reraise_exception():
            LOG.exception(_LE("Failed to set boot device %(boot_device)s for "
                              "node %(node_id)s with error: %(error)s."),
                          {'boot_device': boot_device, 'node_id': node.uuid,
                           'error': e})
    else:
        LOG.info(_LI("Successfully set boot device %(boot_device)s for "
                     "node %(node_id)s"),
                 {'boot_device': boot_device, 'node_id': node.uuid})


def _generate_enable_boot_config_input():
    """Generate Xmldoc as enable_boot_config input.

    This generates a Xmldoc used as input for enable_boot_config.

    :returns: Xmldoc.
    """
    method_input = "SetBootConfigRole_INPUT"
    namespace = resource_uris.CIM_BootService
    doc = pywsman.XmlDoc(method_input)
    root = doc.root()
    root.set_ns(namespace)

    child = root.add(namespace, 'BootConfigSetting', None)
    child.add(_ADDRESS, 'Address', _ANONYMOUS)

    grand_child = child.add(_ADDRESS, 'ReferenceParameters', None)
    grand_child.add(_WSMAN, 'ResourceURI', resource_uris.CIM_BootConfigSetting)
    g_grand_child = grand_child.add(_WSMAN, 'SelectorSet', None)
    g_g_grand_child = g_grand_child.add(_WSMAN, 'Selector',
                                        'Intel(r) AMT: Boot Configuration 0')
    g_g_grand_child.attr_add(_WSMAN, 'Name', 'InstanceID')
    root.add(namespace, 'Role', '1')
    return doc


def _enable_boot_config(node):
    """Enable boot configuration of AMT Client.

    :param node: a node object
    :raises: AMTFailure
    :raises: AMTConnectFailure
    """
    amt_common.awake_amt_interface(node)
    client = amt_common.get_wsman_client(node)
    method = 'SetBootConfigRole'
    doc = _generate_enable_boot_config_input()
    options = pywsman.ClientOptions()
    options.add_selector('Name', 'Intel(r) AMT Boot Service')
    try:
        client.wsman_invoke(options, resource_uris.CIM_BootService,
                            method, doc)
    except (exception.AMTFailure, exception.AMTConnectFailure) as e:
        with excutils.save_and_reraise_exception():
            LOG.exception(_LE("Failed to enable boot config for node "
                              "%(node_id)s with error: %(error)s."),
                          {'node_id': node.uuid, 'error': e})
    else:
        LOG.info(_LI("Successfully enabled boot config for node %(node_id)s."),
                 {'node_id': node.uuid})


class AMTManagement(base.ManagementInterface):

    def get_properties(self):
        return copy.deepcopy(amt_common.COMMON_PROPERTIES)

    def validate(self, task):
        """Validate the driver_info in the node

        Check if the driver_info contains correct required fields

        :param task: a TaskManager instance contains the target node
        :raises: MissingParameterValue if any required parameters are missing.
        :raises: InvalidParameterValue if any parameters have invalid values.
        """
        # FIXME(lintan): validate hangs if unable to reach AMT, so dont
        # connect to the node until bug 1314961 is resolved.
        amt_common.parse_driver_info(task.node)

    def get_supported_boot_devices(self, task):
        """Get a list of the supported boot devices.

        :param task: a task from TaskManager.
        :returns: A list with the supported boot devices.
        """
        return list(amt_common.BOOT_DEVICES_MAPPING)

    @task_manager.require_exclusive_lock
    def set_boot_device(self, task, device, persistent=False):
        """Set the boot device for the task's node.

        Set the boot device to use on next boot of the node.

        :param task: a task from TaskManager.
        :param device: the boot device
        :param persistent: Boolean value. True if the boot device will
                           persist to all future boots, False if not.
                           Default: False.
        :raises: InvalidParameterValue if an invalid boot device is specified.
        """
        node = task.node

        if device not in amt_common.BOOT_DEVICES_MAPPING:
            raise ironic_exception.InvalidParameterValue(
                _("set_boot_device called with invalid device "
                  "%(device)s for node %(node_id)s."
                  ) % {'device': device, 'node_id': node.uuid})

        # AMT/vPro doesn't support set boot_device persistent, so we have to
        # save amt_boot_device/amt_boot_persistent in driver_internal_info.
        driver_internal_info = node.driver_internal_info
        driver_internal_info['amt_boot_device'] = device
        driver_internal_info['amt_boot_persistent'] = persistent
        node.driver_internal_info = driver_internal_info
        node.save()

    def get_boot_device(self, task):
        """Get the current boot device for the task's node.

        Returns the current boot device of the node.

        :param task: a task from TaskManager.
        :returns: a dictionary containing:

             :boot_device: the boot device
             :persistent: Whether the boot device will persist to all
                future boots or not, None if it is unknown.

        """
        driver_internal_info = task.node.driver_internal_info
        device = driver_internal_info.get('amt_boot_device')
        persistent = driver_internal_info.get('amt_boot_persistent')
        if not device:
            device = amt_common.DEFAULT_BOOT_DEVICE
            persistent = True
        return {'boot_device': device,
                'persistent': persistent}

    def ensure_next_boot_device(self, node, boot_device):
        """Set next boot device (one time only) of AMT Client.

        :param node: a node object
        :param boot_device: the boot device
        :raises: AMTFailure
        :raises: AMTConnectFailure
        """
        driver_internal_info = node.driver_internal_info
        if not driver_internal_info.get('amt_boot_persistent'):
            driver_internal_info['amt_boot_device'] = (
                amt_common.DEFAULT_BOOT_DEVICE)
            driver_internal_info['amt_boot_persistent'] = True
            node.driver_internal_info = driver_internal_info
            node.save()

        _set_boot_device_order(node, boot_device)
        _enable_boot_config(node)

    def get_sensors_data(self, task):
        raise NotImplementedError()
