# Copyright (c) 2013 eBay Inc.
# Copyright (c) 2013 OpenStack Foundation
#
#    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.

"""The QoS Specs Implementation"""


from oslo_db import exception as db_exc
from oslo_log import log as logging

from cinder import context
from cinder import db
from cinder import exception
from cinder.i18n import _
from cinder import objects
from cinder import utils
from cinder.volume import volume_types


LOG = logging.getLogger(__name__)

CONTROL_LOCATION = ['front-end', 'back-end', 'both']


def create(context, name, specs=None):
    """Creates qos_specs.

    :param specs: Dictionary that contains specifications for QoS

    Expected format of the input parameter:

       .. code-block:: python

          {
             'consumer': 'front-end',
             'total_iops_sec': 1000,
             'total_bytes_sec': 1024000
          }

    """
    # Validate the key-value pairs in the qos spec.
    utils.validate_dictionary_string_length(specs)

    consumer = specs.get('consumer')
    if consumer:
        # If we need to modify specs, copy so we don't cause unintended
        # consequences for the caller
        specs = specs.copy()
        del specs['consumer']

    values = dict(name=name, consumer=consumer, specs=specs)

    LOG.debug("Dict for qos_specs: %s", values)
    qos_spec = objects.QualityOfServiceSpecs(context, **values)
    qos_spec.create()
    return qos_spec


def update(context, qos_specs_id, specs):
    """Update qos specs.

    :param specs: dictionary that contains key/value pairs for updating
                  existing specs.
          e.g. {'consumer': 'front-end',
                'total_iops_sec': 500,
                'total_bytes_sec': 512000,}
    """
    LOG.debug('qos_specs.update(): specs %s', specs)

    try:
        utils.validate_dictionary_string_length(specs)
        qos_spec = objects.QualityOfServiceSpecs.get_by_id(context,
                                                           qos_specs_id)

        if 'consumer' in specs:
            qos_spec.consumer = specs['consumer']
            # If we need to modify specs, copy so we don't cause unintended
            # consequences for the caller
            specs = specs.copy()
            del specs['consumer']

        # Update any values in specs dict
        qos_spec.specs.update(specs)

        qos_spec.save()
    except exception.InvalidInput as e:
        raise exception.InvalidQoSSpecs(reason=e)
    except db_exc.DBError:
        LOG.exception('DB error:')
        raise exception.QoSSpecsUpdateFailed(specs_id=qos_specs_id,
                                             qos_specs=specs)

    return qos_spec


def delete(context, qos_specs_id, force=False):
    """Marks qos specs as deleted.

    'force' parameter is a flag to determine whether should destroy
    should continue when there were entities associated with the qos specs.
    force=True indicates caller would like to mark qos specs as deleted
    even if there was entities associate with target qos specs.
    Trying to delete a qos specs still associated with entities will
    cause QoSSpecsInUse exception if force=False (default).
    """
    if qos_specs_id is None:
        msg = _("id cannot be None")
        raise exception.InvalidQoSSpecs(reason=msg)

    qos_spec = objects.QualityOfServiceSpecs.get_by_id(
        context, qos_specs_id)

    qos_spec.destroy(force)


def delete_keys(context, qos_specs_id, keys):
    """Marks specified key of target qos specs as deleted."""
    if qos_specs_id is None:
        msg = _("id cannot be None")
        raise exception.InvalidQoSSpecs(reason=msg)

    qos_spec = objects.QualityOfServiceSpecs.get_by_id(context, qos_specs_id)

    # Previous behavior continued to delete keys until it hit first unset one,
    # so for now will mimic that. In the future it would be useful to have all
    # or nothing deletion of keys (or at least delete all set keys),
    # especially since order of keys from CLI to API is not preserved currently
    try:
        for key in keys:
            try:
                del qos_spec.specs[key]
            except KeyError:
                raise exception.QoSSpecsKeyNotFound(
                    specs_key=key, specs_id=qos_specs_id)
    finally:
        qos_spec.save()


def get_associations(context, qos_specs_id):
    """Get all associations of given qos specs."""
    try:
        types = objects.VolumeTypeList.get_all_types_for_qos(context,
                                                             qos_specs_id)
    except db_exc.DBError:
        LOG.exception('DB error:')
        msg = _('Failed to get all associations of '
                'qos specs %s') % qos_specs_id
        LOG.warning(msg)
        raise exception.CinderException(message=msg)

    result = []
    for vol_type in types:
        result.append({
            'association_type': 'volume_type',
            'name': vol_type.name,
            'id': vol_type.id
        })

    return result


def associate_qos_with_type(context, specs_id, type_id):
    """Associate qos_specs with volume type.

    Associate target qos specs with specific volume type.

    :param specs_id: qos specs ID to associate with
    :param type_id: volume type ID to associate with
    :raises VolumeTypeNotFound: if volume type doesn't exist
    :raises QoSSpecsNotFound: if qos specs doesn't exist
    :raises InvalidVolumeType: if volume type is already associated
                               with qos specs other than given one.
    :raises QoSSpecsAssociateFailed: if there was general DB error
    """
    try:
        get_qos_specs(context, specs_id)
        res = volume_types.get_volume_type_qos_specs(type_id)
        if res.get('qos_specs', None):
            if res['qos_specs'].get('id') != specs_id:
                msg = (_("Type %(type_id)s is already associated with another "
                         "qos specs: %(qos_specs_id)s") %
                       {'type_id': type_id,
                        'qos_specs_id': res['qos_specs']['id']})
                raise exception.InvalidVolumeType(reason=msg)
        else:
            db.qos_specs_associate(context, specs_id, type_id)
    except db_exc.DBError:
        LOG.exception('DB error:')
        LOG.warning('Failed to associate qos specs '
                    '%(id)s with type: %(vol_type_id)s',
                    dict(id=specs_id, vol_type_id=type_id))
        raise exception.QoSSpecsAssociateFailed(specs_id=specs_id,
                                                type_id=type_id)


def disassociate_qos_specs(context, specs_id, type_id):
    """Disassociate qos_specs from volume type."""
    try:
        get_qos_specs(context, specs_id)
        db.qos_specs_disassociate(context, specs_id, type_id)
    except db_exc.DBError:
        LOG.exception('DB error:')
        LOG.warning('Failed to disassociate qos specs '
                    '%(id)s with type: %(vol_type_id)s',
                    dict(id=specs_id, vol_type_id=type_id))
        raise exception.QoSSpecsDisassociateFailed(specs_id=specs_id,
                                                   type_id=type_id)


def disassociate_all(context, specs_id):
    """Disassociate qos_specs from all entities."""
    try:
        get_qos_specs(context, specs_id)
        db.qos_specs_disassociate_all(context, specs_id)
    except db_exc.DBError:
        LOG.exception('DB error:')
        LOG.warning('Failed to disassociate qos specs %s.', specs_id)
        raise exception.QoSSpecsDisassociateFailed(specs_id=specs_id,
                                                   type_id=None)


def get_all_specs(context, filters=None, marker=None, limit=None, offset=None,
                  sort_keys=None, sort_dirs=None):
    """Get all non-deleted qos specs."""
    return objects.QualityOfServiceSpecsList.get_all(
        context, filters=filters, marker=marker, limit=limit, offset=offset,
        sort_keys=sort_keys, sort_dirs=sort_dirs)


def get_qos_specs(ctxt, spec_id):
    """Retrieves single qos specs by id."""
    if spec_id is None:
        msg = _("id cannot be None")
        raise exception.InvalidQoSSpecs(reason=msg)

    if ctxt is None:
        ctxt = context.get_admin_context()

    return objects.QualityOfServiceSpecs.get_by_id(ctxt, spec_id)
