# 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 json
import os

from ironic.common import exception
from ironic.drivers import base
from ironic.drivers.modules import ipmitool
from ironic_lib import utils as ironic_utils
import jsonschema
from jsonschema import exceptions as json_schema_exc
from oslo_config import cfg
from oslo_log import log
from oslo_utils import excutils
import six

from ironic_staging_drivers.common.i18n import _
from ironic_staging_drivers.common.i18n import _LE
from ironic_staging_drivers.common.i18n import _LI
from ironic_staging_drivers.intel_nm import nm_commands


CONF = cfg.CONF
CONF.import_opt('tempdir', 'ironic.common.utils')
LOG = log.getLogger(__name__)

SCHEMAS = ('control_schema', 'get_cap_schema', 'main_ids_schema',
           'policy_schema', 'suspend_schema', 'statistics_schema')


def _command_to_string(cmd):
    """Convert a list with command raw bytes to string."""
    return ' '.join(cmd)


def _get_nm_address(task):
    """Get Intel Node Manager target channel and address.

    :param task: a TaskManager instance.
    :raises: IPMIFailure if Intel Node Manager is not detected on a node or if
             an error happens during detection.
    :returns: a tuple with IPMI channel and address of Intel Node Manager.
    """
    node = task.node
    driver_internal_info = node.driver_internal_info

    def _save_to_node(channel, address):
        driver_internal_info['intel_nm_channel'] = channel
        driver_internal_info['intel_nm_address'] = address
        node.driver_internal_info = driver_internal_info
        node.save()

    channel = driver_internal_info.get('intel_nm_channel')
    address = driver_internal_info.get('intel_nm_address')
    if channel and address:
        return channel, address
    if channel is False and address is False:
        raise exception.IPMIFailure(_('Driver data indicates that Intel '
                                      'Node Manager detection failed.'))
    LOG.info(_LI('Start detection of Intel Node Manager on node %s'),
             node.uuid)
    sdr_filename = os.path.join(CONF.tempdir, node.uuid + '.sdr')
    res = None
    try:
        ipmitool.dump_sdr(task, sdr_filename)
        res = nm_commands.parse_slave_and_channel(sdr_filename)
    finally:
        ironic_utils.unlink_without_raise(sdr_filename)
    if res is None:
        _save_to_node(False, False)
        raise exception.IPMIFailure(_('Intel Node Manager is not detected.'))
    address, channel = res
    LOG.debug('Intel Node Manager sensors present in SDR on node %(node)s, '
              'channel %(channel)s address %(address)s.',
              {'node': node.uuid, 'channel': channel, 'address': address})
    # SDR can contain wrong info, try simple command
    node.driver_info['ipmi_bridging'] = 'single'
    node.driver_info['ipmi_target_channel'] = channel
    node.driver_info['ipmi_target_address'] = address
    try:
        ipmitool.send_raw(task,
                          _command_to_string(nm_commands.get_version(None)))
        _save_to_node(channel, address)
        return channel, address
    except exception.IPMIFailure:
        _save_to_node(False, False)
        raise exception.IPMIFailure(_('Intel Node Manager sensors record '
                                      'present in SDR but Node Manager is not '
                                      'responding.'))


def _execute_nm_command(task, data, command_func, parse_func=None):
    """Execute Intel Node Manager command via send_raw().

    :param task: a TaskManager instance.
    :param data: a dict with data passed to vendor's method.
    :param command_func: a function that returns raw command bytes.
    :param parse_func: a function that parses returned raw bytes.
    :raises: IPMIFailure if Intel Node Manager is not detected on a node or if
             an error happens during command execution.
    :returns: a dict with parsed output or None if command does not return
              user's info.
    """
    try:
        channel, address = _get_nm_address(task)
    except exception.IPMIFailure as e:
        with excutils.save_and_reraise_exception():
            LOG.exception(_LE('Can not obtain Intel Node Manager address for '
                              'node %(node)s: %(err)s'),
                          {'node': task.node.uuid, 'err': six.text_type(e)})
    driver_info = task.node.driver_info
    driver_info['ipmi_bridging'] = 'single'
    driver_info['ipmi_target_channel'] = channel
    driver_info['ipmi_target_address'] = address
    cmd = _command_to_string(command_func(data))
    out = ipmitool.send_raw(task, cmd)[0]
    if parse_func:
        try:
            return parse_func(out.split())
        except exception.IPMIFailure as e:
            with excutils.save_and_reraise_exception():
                LOG.exception(_LE('Error in returned data for node %(node)s: '
                                  '%(err)s'), {'node': task.node.uuid,
                                               'err': six.text_type(e)})


class IntelNMVendorPassthru(base.VendorInterface):
    """Intel Node Manager policies vendor interface."""

    def __init__(self):
        schemas_dir = os.path.dirname(__file__)
        for schema in SCHEMAS:
            filename = os.path.join(schemas_dir, schema + '.json')
            with open(filename, 'r') as sf:
                setattr(self, schema, json.load(sf))

    def _validate_policy_methods(self, method, **kwargs):
        if method in ('get_nm_policy', 'remove_nm_policy',
                      'get_nm_policy_suspend', 'remove_nm_policy_suspend'):
            jsonschema.validate(kwargs, self.main_ids_schema)

        elif method == 'control_nm_policy':
            jsonschema.validate(kwargs, self.control_schema)
            if kwargs['scope'] != 'global' and 'domain_id' not in kwargs:
                raise exception.MissingParameterValue(_('Missing "domain_id"'))
            if kwargs['scope'] == 'policy' and 'policy_id' not in kwargs:
                raise exception.MissingParameterValue(_('Missing "policy_id"'))

        elif method == 'set_nm_policy':
            jsonschema.validate(kwargs, self.policy_schema)
            if kwargs['policy_trigger'] == 'boot':
                if not isinstance(kwargs['target_limit'], dict):
                    raise exception.InvalidParameterValue(_('Invalid boot '
                                                            'policy'))
            elif 'correction_time' not in kwargs:
                raise exception.MissingParameterValue(
                    _('Missing "correction_time" for no-boot policy'))

        elif method == 'set_nm_policy_suspend':
            jsonschema.validate(kwargs, self.suspend_schema)

        elif method == 'get_nm_capabilities':
            jsonschema.validate(kwargs, self.get_cap_schema)

    def _validate_statistics_methods(self, method, **kwargs):
        jsonschema.validate(kwargs, self.statistics_schema)

        global_params = ('unhandled_requests', 'response_time',
                         'cpu_throttling', 'memory_throttling',
                         'communication_failures')

        if kwargs['scope'] == 'policy' and 'policy_id' not in kwargs:
                raise exception.MissingParameterValue(_('Missing "policy_id"'))

        if kwargs.get('parameter_name') not in global_params:
            if 'domain_id' not in kwargs:
                raise exception.MissingParameterValue(_('Missing "domain_id"'))

        if method == 'reset_nm_statistics':
            if 'parameter_name' in kwargs:
                if kwargs['parameter_name'] not in global_params:
                    raise exception.InvalidParameterValue(
                        _('Invalid parameter name for resetting statistic, '
                          'individual reset is possible only for: %s') %
                        ', '.join(global_params))

        elif method == 'get_nm_statistics':
            if 'parameter_name' not in kwargs:
                raise exception.MissingParameterValue(
                    _('Parameter name is mandatory for getting statistics'))
            # valid parameters depend on scope
            if (kwargs['parameter_name'] not in
                nm_commands.STATISTICS[kwargs['scope']]):
                    raise exception.InvalidParameterValue(
                        _('Invalid parameter name %(param)% for scope '
                          '%(scope)s') % {'param': kwargs['parameter_name'],
                                          'scope': kwargs['scope']})

    def get_properties(self):
        """Returns the properties of the interface.."""
        return {}

    def validate(self, task, method, http_method, **kwargs):
        """Validates the vendor method's parameters.

        This method validates whether the supplied data contains the required
        information for the driver.

        :param task: a TaskManager instance.
        :param method: name of vendor method.
        :param http_method: HTTP method.
        :param kwargs: data passed to vendor's method.
        :raises: InvalidParameterValue if supplied data is not valid.
        :raises: MissingParameterValue if parameters missing in supplied data.
        """
        try:
            if 'statistics' in method:
                self._validate_statistics_methods(method, **kwargs)
            else:
                self._validate_policy_methods(method, **kwargs)
        except json_schema_exc.ValidationError as e:
            raise exception.InvalidParameterValue(_('Input data validation '
                                                    'error: %s') % e)

    @base.passthru(['PUT'])
    def control_nm_policy(self, task, **kwargs):
        """Enable or disable Intel Node Manager policy control.

        :param task: a TaskManager instance.
        :param kwargs: data passed to method.
        :raises: IPMIFailure on an error.
        """
        _execute_nm_command(task, kwargs, nm_commands.control_policies)

    @base.passthru(['PUT'])
    def set_nm_policy(self, task, **kwargs):
        """Set Intel Node Manager policy.

        :param task: a TaskManager instance.
        :param kwargs: data passed to method.
        :raises: IPMIFailure on an error.
        """
        _execute_nm_command(task, kwargs, nm_commands.set_policy)

    @base.passthru(['GET'], async=False)
    def get_nm_policy(self, task, **kwargs):
        """Get Intel Node Manager policy.

        :param task: a TaskManager instance.
        :param kwargs: data passed to method.
        :raises: IPMIFailure on an error.
        :returns: a dictionary containing policy settings.
        """
        return _execute_nm_command(task, kwargs, nm_commands.get_policy,
                                   nm_commands.parse_policy)

    @base.passthru(['DELETE'])
    def remove_nm_policy(self, task, **kwargs):
        """Remove Intel Node Manager policy.

        :param task: a TaskManager instance.
        :param kwargs: data passed to method.
        :raises: IPMIFailure on an error.
        """
        _execute_nm_command(task, kwargs, nm_commands.remove_policy)

    @base.passthru(['PUT'])
    def set_nm_policy_suspend(self, task, **kwargs):
        """Set Intel Node Manager policy suspend periods.

        :param task: a TaskManager instance.
        :param kwargs: data passed to method.
        :raises: IPMIFailure on an error.
        """
        _execute_nm_command(task, kwargs, nm_commands.set_policy_suspend)

    @base.passthru(['GET'], async=False)
    def get_nm_policy_suspend(self, task, **kwargs):
        """Get Intel Node Manager policy suspend periods.

        :param task: a TaskManager instance.
        :param kwargs: data passed to method.
        :raises: IPMIFailure on an error.
        :returns: a dictionary containing suspend info for a policy.
        """
        return _execute_nm_command(task, kwargs,
                                   nm_commands.get_policy_suspend,
                                   nm_commands.parse_policy_suspend)

    @base.passthru(['DELETE'])
    def remove_nm_policy_suspend(self, task, **kwargs):
        """Remove Intel Node Manager policy suspend periods.

        :param task: a TaskManager instance.
        :param kwargs: data passed to method.
        :raises: IPMIFailure on an error.
        """
        _execute_nm_command(task, kwargs, nm_commands.remove_policy_suspend)

    @base.passthru(['GET'], async=False)
    def get_nm_capabilities(self, task, **kwargs):
        """Get Intel Node Manager capabilities.

        :param task: a TaskManager instance.
        :param kwargs: data passed to method.
        :raises: IPMIFailure on an error.
        :returns: a dictionary containing Intel NM capabilities.
        """
        return _execute_nm_command(task, kwargs, nm_commands.get_capabilities,
                                   nm_commands.parse_capabilities)

    @base.passthru(['GET'], async=False)
    def get_nm_version(self, task, **kwargs):
        """Get Intel Node Manager version.

        :param task: a TaskManager instance.
        :param kwargs: data passed to method.
        :raises: IPMIFailure on an error.
        :returns: a dictionary containing Intel NM version.
        """
        return _execute_nm_command(task, kwargs, nm_commands.get_version,
                                   nm_commands.parse_version)

    @base.passthru(['GET'], async=False)
    def get_nm_statistics(self, task, **kwargs):
        """Get Intel Node Manager statistics.

        :param task: a TaskManager instance.
        :param kwargs: data passed to method.
        :raises: IPMIFailure on an error.
        :returns: a dictionary containing statistics info.
        """
        return _execute_nm_command(task, kwargs,
                                   nm_commands.get_statistics,
                                   nm_commands.parse_statistics)

    @base.passthru(['PUT'])
    def reset_nm_statistics(self, task, **kwargs):
        """Reset Intel Node Manager statistics.

        :param task: a TaskManager instance.
        :param kwargs: data passed to method.
        :raises: IPMIFailure on an error.
        """
        _execute_nm_command(task, kwargs, nm_commands.reset_statistics)
