# Copyright (c) 2011 Zadara Storage Inc.
# Copyright (c) 2011 OpenStack Foundation
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# Copyright (c) 2010 Citrix Systems, Inc.
# Copyright 2011 Ken Pepple
#
#    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.

"""Built-in volume type properties."""

from __future__ import annotations

from typing import Any, Iterable, Optional, Union

from oslo_config import cfg
from oslo_db import exception as db_exc
from oslo_log import log as logging
from oslo_utils import uuidutils

from cinder import context
from cinder import db
from cinder import exception
from cinder.i18n import _
from cinder import quota
from cinder import rpc
from cinder import utils

CONF = cfg.CONF
LOG = logging.getLogger(__name__)
QUOTAS = quota.QUOTAS
ENCRYPTION_IGNORED_FIELDS = ('volume_type_id', 'created_at', 'updated_at',
                             'deleted_at', 'encryption_id')
QOS_IGNORED_FIELDS = ('id', 'name', 'created_at', 'updated_at', 'deleted_at')
DEFAULT_VOLUME_TYPE = "__DEFAULT__"

MIN_SIZE_KEY = "provisioning:min_vol_size"
MAX_SIZE_KEY = "provisioning:max_vol_size"


def create(context: context.RequestContext,
           name: str,
           extra_specs: Optional[dict[str, Any]] = None,
           is_public: bool = True,
           projects: Optional[list[str]] = None,
           description: Optional[str] = None):
    """Creates volume types."""
    extra_specs = extra_specs or {}
    projects = projects or []
    elevated = context if context.is_admin else context.elevated()
    try:
        type_ref = db.volume_type_create(elevated,
                                         dict(name=name,
                                              extra_specs=extra_specs,
                                              is_public=is_public,
                                              description=description),
                                         projects=projects)
    except db_exc.DBError:
        LOG.exception('DB error:')
        raise exception.VolumeTypeCreateFailed(name=name,
                                               extra_specs=extra_specs)
    return type_ref


def update(context: context.RequestContext,
           id: Optional[str],
           name: Optional[str],
           description: Optional[str],
           is_public: Optional[bool] = None) -> None:
    """Update volume type by id."""
    if id is None:
        msg = _("id cannot be None")
        raise exception.InvalidVolumeType(reason=msg)
    elevated = context if context.is_admin else context.elevated()
    old_volume_type = get_volume_type(elevated, id)
    try:
        db.volume_type_update(elevated, id,
                              dict(name=name, description=description,
                                   is_public=is_public))
        # Rename resource in quota if volume type name is changed.
        if name:
            old_type_name = old_volume_type.get('name')
            if old_type_name != name:
                old_description = old_volume_type.get('description')
                old_public = old_volume_type.get('is_public')
                try:
                    QUOTAS.update_quota_resource(elevated,
                                                 old_type_name,
                                                 name)
                # Rollback the updated information to the original
                except db_exc.DBError:
                    db.volume_type_update(elevated, id,
                                          dict(name=old_type_name,
                                               description=old_description,
                                               is_public=old_public))
                    raise
    except db_exc.DBError:
        LOG.exception('DB error:')
        raise exception.VolumeTypeUpdateFailed(id=id)


def destroy(context: context.RequestContext, id: str) -> dict[str, Any]:
    """Marks volume types as deleted.

    There must exist at least one volume type (i.e. the default type) in
    the deployment.
    This method achieves that by ensuring:
    1) the default_volume_type is set and is a valid one
    2) the type requested to delete isn't the default type

    :raises VolumeTypeDefaultDeletionError: when the type requested to
                                            delete is the default type
    """
    if id is None:
        msg = _("id cannot be None")
        raise exception.InvalidVolumeType(reason=msg)

    projects_with_default_type = db.get_all_projects_with_default_type(
        context.elevated(), id)
    if len(projects_with_default_type) > 0:
        # don't allow delete if the type requested is a project default
        project_list = [p.project_id for p in projects_with_default_type]
        LOG.exception('Default type with %(volume_type_id)s is associated '
                      'with projects %(projects)s',
                      {'volume_type_id': id,
                       'projects': project_list})
        raise exception.VolumeTypeDefaultDeletionError(volume_type_id=id)

    # Default type *must* be set in order to delete any volume type.
    # If the default isn't set, the following call will raise
    # VolumeTypeDefaultMisconfiguredError exception which will error out the
    # delete operation.
    default_type = get_default_volume_type()
    # don't allow delete if the type requested is the conf default type
    if id == default_type.get('id'):
        raise exception.VolumeTypeDefaultDeletionError(volume_type_id=id)

    elevated = context if context.is_admin else context.elevated()
    return db.volume_type_destroy(elevated, id)


def get_all_types(context: context.RequestContext,
                  inactive: int = 0,
                  filters: Optional[dict] = None,
                  marker: Optional[dict[str, Any]] = None,
                  limit: Optional[int] = None,
                  sort_keys: Optional[list[str]] = None,
                  sort_dirs: Optional[list[str]] = None,
                  offset: Optional[int] = None,
                  list_result: bool = False) -> Union[dict[str, Any], list]:
    """Get all non-deleted volume_types.

    Pass true as argument if you want deleted volume types returned also.

    """
    vol_types = db.volume_type_get_all(context, inactive, filters=filters,
                                       marker=marker, limit=limit,
                                       sort_keys=sort_keys,
                                       sort_dirs=sort_dirs, offset=offset,
                                       list_result=list_result)
    return vol_types


def get_volume_type(
        ctxt: Optional[context.RequestContext],
        id: Optional[str],
        expected_fields: Optional[Iterable[str]] = None) -> dict[str, Any]:
    """Retrieves single volume type by id."""
    if id is None:
        msg = _("id cannot be None")
        raise exception.InvalidVolumeType(reason=msg)

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

    return db.volume_type_get(ctxt, id, expected_fields=expected_fields)


def get_by_name_or_id(context: context.RequestContext,
                      identity: str) -> dict[str, Any]:
    """Retrieves volume type by id or name"""
    if uuidutils.is_uuid_like(identity):
        # both name and id can be in uuid format
        try:
            return get_volume_type(context, identity)
        except exception.VolumeTypeNotFound:
            try:
                # A user can create a type with the name in a UUID format,
                # so here we check for the uuid-like name.
                return get_volume_type_by_name(context, identity)
            except exception.VolumeTypeNotFoundByName:
                raise exception.VolumeTypeNotFound(volume_type_id=identity)
    return get_volume_type_by_name(context, identity)


def get_volume_type_by_name(context: context.RequestContext,
                            name: Optional[str]) -> dict[str, Any]:
    """Retrieves single volume type by name."""
    if name is None:
        msg = _("name cannot be None")
        raise exception.InvalidVolumeType(reason=msg)

    return db.volume_type_get_by_name(context, name)


def get_default_volume_type(
        contxt: Optional[context.RequestContext] = None) -> dict[str, Any]:
    """Get the default volume type.

    :raises VolumeTypeDefaultMisconfiguredError: when the configured default
                                                 is not found
    """

    if contxt:
        project_default = db.project_default_volume_type_get(
            contxt, contxt.project_id)
        if project_default:
            return get_volume_type(contxt, project_default.volume_type_id)
    name = CONF.default_volume_type
    ctxt = context.get_admin_context()
    vol_type = {}
    try:
        vol_type = get_volume_type_by_name(ctxt, name)
    except (exception.VolumeTypeNotFoundByName, exception.InvalidVolumeType):
        # Couldn't find volume type with the name in default_volume_type
        # flag, record this issue and raise exception
        LOG.exception('Default volume type is not found. '
                      'Please check default_volume_type config:')
        raise exception.VolumeTypeDefaultMisconfiguredError(
            volume_type_name=name)

    return vol_type


def get_volume_type_extra_specs(
        volume_type_id: str,
        key: Union[str, bool] = False) -> Union[dict, bool]:
    volume_type = get_volume_type(context.get_admin_context(),
                                  volume_type_id)
    extra_specs = volume_type['extra_specs']
    if key:
        if extra_specs.get(key):
            return extra_specs.get(key)
        else:
            return False
    else:
        return extra_specs


def is_public_volume_type(context: context.RequestContext,
                          volume_type_id: str) -> bool:
    """Return is_public boolean value of volume type"""
    volume_type = db.volume_type_get(context, volume_type_id)
    return volume_type['is_public']


@utils.if_notifications_enabled
def notify_about_volume_type_access_usage(context: context.RequestContext,
                                          volume_type_id: str,
                                          project_id: str,
                                          event_suffix: str,
                                          host: Optional[str] = None) -> None:
    """Notify about successful usage type-access-(add/remove) command.

    :param context: security context
    :param volume_type_id: volume type uuid
    :param project_id: tenant uuid
    :param event_suffix: name of called operation access-(add/remove)
    :param host: hostname
    """
    notifier_info = {'volume_type_id': volume_type_id,
                     'project_id': project_id}

    if not host:
        host = CONF.host

    notifier = rpc.get_notifier("volume_type_project", host)
    notifier.info(context,
                  'volume_type_project.%s' % event_suffix,
                  notifier_info)


def add_volume_type_access(context: context.RequestContext,
                           volume_type_id: Optional[str],
                           project_id: str) -> None:
    """Add access to volume type for project_id."""
    if volume_type_id is None:
        msg = _("volume_type_id cannot be None")
        raise exception.InvalidVolumeType(reason=msg)
    elevated = context if context.is_admin else context.elevated()
    if is_public_volume_type(elevated, volume_type_id):
        msg = _("Type access modification is not applicable to public volume "
                "type.")
        raise exception.InvalidVolumeType(reason=msg)

    db.volume_type_access_add(elevated, volume_type_id, project_id)

    notify_about_volume_type_access_usage(context,
                                          volume_type_id,
                                          project_id,
                                          'access.add')


def remove_volume_type_access(context: context.RequestContext,
                              volume_type_id: Optional[str],
                              project_id: str) -> None:
    """Remove access to volume type for project_id."""
    if volume_type_id is None:
        msg = _("volume_type_id cannot be None")
        raise exception.InvalidVolumeType(reason=msg)
    elevated = context if context.is_admin else context.elevated()
    if is_public_volume_type(elevated, volume_type_id):
        msg = _("Type access modification is not applicable to public volume "
                "type.")
        raise exception.InvalidVolumeType(reason=msg)

    db.volume_type_access_remove(elevated, volume_type_id, project_id)

    notify_about_volume_type_access_usage(context,
                                          volume_type_id,
                                          project_id,
                                          'access.remove')


def is_encrypted(context: context.RequestContext,
                 volume_type_id: str) -> bool:
    return get_volume_type_encryption(context, volume_type_id) is not None


def get_volume_type_encryption(
        context: context.RequestContext,
        volume_type_id: Optional[str]) -> Optional[dict]:
    if volume_type_id is None:
        return None

    encryption = db.volume_type_encryption_get(context, volume_type_id)
    return encryption


def get_volume_type_qos_specs(volume_type_id: str) -> dict[str, Any]:
    """Get all qos specs for given volume type."""
    ctxt = context.get_admin_context()
    res = db.volume_type_qos_specs_get(ctxt,
                                       volume_type_id)
    return res


def volume_types_diff(context: context.RequestContext,
                      vol_type_id1: str,
                      vol_type_id2: str) -> tuple[dict[str, Any], bool]:
    """Returns a 'diff' of two volume types and whether they are equal.

    Returns a tuple of (diff, equal), where 'equal' is a boolean indicating
    whether there is any difference, and 'diff' is a dictionary with the
    following format:

    .. code-block:: default

        {
            'extra_specs': {'key1': (value_in_1st_vol_type,
                                     value_in_2nd_vol_type),
                            'key2': (value_in_1st_vol_type,
                                     value_in_2nd_vol_type),
                            {...}}
            'qos_specs': {'key1': (value_in_1st_vol_type,
                                   value_in_2nd_vol_type),
                          'key2': (value_in_1st_vol_type,
                                   value_in_2nd_vol_type),
                          {...}}
            'encryption': {'cipher': (value_in_1st_vol_type,
                                      value_in_2nd_vol_type),
                          {'key_size': (value_in_1st_vol_type,
                                        value_in_2nd_vol_type),
                           {...}}
        }
    """
    def _fix_qos_specs(qos_specs: Optional[dict]) -> None:
        if qos_specs:
            for key in QOS_IGNORED_FIELDS:
                qos_specs.pop(key, None)
            qos_specs.update(qos_specs.pop('specs', {}))

    def _fix_encryption_specs(encryption: Optional[dict]) -> Optional[dict]:
        if encryption:
            encryption = dict(encryption)
            for param in ENCRYPTION_IGNORED_FIELDS:
                encryption.pop(param, None)
        return encryption

    def _dict_diff(dict1: Optional[dict],
                   dict2: Optional[dict]) -> tuple[dict[str, Any], bool]:
        res = {}
        equal = True
        if dict1 is None:
            dict1 = {}
        if dict2 is None:
            dict2 = {}
        for k, v in dict1.items():
            res[k] = (v, dict2.get(k))
            if k not in dict2 or res[k][0] != res[k][1]:
                equal = False
        for k, v in dict2.items():
            res[k] = (dict1.get(k), v)
            if k not in dict1 or res[k][0] != res[k][1]:
                equal = False
        return (res, equal)

    all_equal = True
    diff = {}
    vol_type_data = []
    for vol_type_id in (vol_type_id1, vol_type_id2):
        if vol_type_id is None:
            specs = {'extra_specs': None,
                     'qos_specs': None,
                     'encryption': None}
        else:
            specs = {}
            vol_type = get_volume_type(context, vol_type_id)
            specs['extra_specs'] = vol_type.get('extra_specs')
            qos_specs = get_volume_type_qos_specs(vol_type_id)
            specs['qos_specs'] = qos_specs.get('qos_specs')
            _fix_qos_specs(specs['qos_specs'])
            specs['encryption'] = get_volume_type_encryption(context,
                                                             vol_type_id)
            specs['encryption'] = _fix_encryption_specs(specs['encryption'])
        vol_type_data.append(specs)

    diff['extra_specs'], equal = _dict_diff(vol_type_data[0]['extra_specs'],
                                            vol_type_data[1]['extra_specs'])
    if not equal:
        all_equal = False
    diff['qos_specs'], equal = _dict_diff(vol_type_data[0]['qos_specs'],
                                          vol_type_data[1]['qos_specs'])
    if not equal:
        all_equal = False
    diff['encryption'], equal = _dict_diff(vol_type_data[0]['encryption'],
                                           vol_type_data[1]['encryption'])
    if not equal:
        all_equal = False

    return (diff, all_equal)


def volume_types_encryption_changed(
        context: context.RequestContext,
        vol_type_id1: str, vol_type_id2: str) -> bool:
    """Return whether encryptions of two volume types are same."""
    def _get_encryption(enc: dict) -> dict:
        enc = dict(enc)
        for param in ENCRYPTION_IGNORED_FIELDS:
            enc.pop(param, None)
        return enc

    enc1 = get_volume_type_encryption(context, vol_type_id1)
    enc2 = get_volume_type_encryption(context, vol_type_id2)

    enc1_filtered = _get_encryption(enc1) if enc1 else None
    enc2_filtered = _get_encryption(enc2) if enc2 else None
    return enc1_filtered != enc2_filtered


def provision_filter_on_size(context: context.RequestContext,
                             volume_type: Optional[dict[str, Any]],
                             size: Union[str, int]) -> None:
    """This function filters volume provisioning requests on size limits.

    If a volume type has provisioning size min/max set, this filter
    will ensure that the volume size requested is within the size
    limits specified in the volume type.
    """

    if not volume_type:
        volume_type = get_default_volume_type()

    if volume_type:
        size_int = int(size)
        extra_specs = volume_type.get('extra_specs', {})
        min_size = extra_specs.get(MIN_SIZE_KEY)
        if min_size and size_int < int(min_size):
            msg = _("Specified volume size of '%(req_size)d' is less "
                    "than the minimum required size of '%(min_size)s' "
                    "for volume type '%(vol_type)s'.") % {
                'req_size': size_int, 'min_size': min_size,
                'vol_type': volume_type['name']
            }
            raise exception.InvalidInput(reason=msg)

        max_size = extra_specs.get(MAX_SIZE_KEY)
        if max_size and size_int > int(max_size):
            msg = _("Specified volume size of '%(req_size)d' is "
                    "greater than the maximum allowable size of "
                    "'%(max_size)s' for volume type '%(vol_type)s'."
                    ) % {
                'req_size': size_int, 'max_size': max_size,
                'vol_type': volume_type['name']}
            raise exception.InvalidInput(reason=msg)
