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


from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import timeutils
from oslo_utils import units
import six
import taskflow.engines
from taskflow.patterns import linear_flow
from taskflow.types import failure as ft

from cinder import exception
from cinder import flow_utils
from cinder.i18n import _, _LE, _LW
from cinder import objects
from cinder.objects import fields
from cinder import policy
from cinder import quota
from cinder import quota_utils
from cinder import utils
from cinder.volume.flows import common
from cinder.volume import utils as vol_utils
from cinder.volume import volume_types

LOG = logging.getLogger(__name__)

ACTION = 'volume:create'
CONF = cfg.CONF
GB = units.Gi
QUOTAS = quota.QUOTAS

# Only in these 'sources' status can we attempt to create a volume from a
# source volume or a source snapshot, other status states we can not create
# from, 'error' being the common example.
SNAPSHOT_PROCEED_STATUS = (fields.SnapshotStatus.AVAILABLE,)
SRC_VOL_PROCEED_STATUS = ('available', 'in-use',)
REPLICA_PROCEED_STATUS = ('active', 'active-stopped',)
CG_PROCEED_STATUS = ('available', 'creating',)
CGSNAPSHOT_PROCEED_STATUS = ('available',)
GROUP_PROCEED_STATUS = ('available', 'creating',)


class ExtractVolumeRequestTask(flow_utils.CinderTask):
    """Processes an api request values into a validated set of values.

    This tasks responsibility is to take in a set of inputs that will form
    a potential volume request and validates those values against a set of
    conditions and/or translates those values into a valid set and then returns
    the validated/translated values for use by other tasks.

    Reversion strategy: N/A
    """

    # This task will produce the following outputs (said outputs can be
    # saved to durable storage in the future so that the flow can be
    # reconstructed elsewhere and continued).
    default_provides = set(['availability_zone', 'size', 'snapshot_id',
                            'source_volid', 'volume_type', 'volume_type_id',
                            'encryption_key_id', 'source_replicaid',
                            'consistencygroup_id', 'cgsnapshot_id',
                            'qos_specs', 'group_id'])

    def __init__(self, image_service, availability_zones, **kwargs):
        super(ExtractVolumeRequestTask, self).__init__(addons=[ACTION],
                                                       **kwargs)
        self.image_service = image_service
        self.availability_zones = availability_zones

    @staticmethod
    def _extract_resource(resource, allowed_vals, exc, resource_name,
                          props=('status',)):
        """Extracts the resource id from the provided resource.

        This method validates the input resource dict and checks that the
        properties which names are passed in `props` argument match
        corresponding lists in `allowed` argument. In case of mismatch
        exception of type exc is raised.

        :param resource: Resource dict.
        :param allowed_vals: Tuple of allowed values lists.
        :param exc: Exception type to raise.
        :param resource_name: Name of resource - used to construct log message.
        :param props: Tuple of resource properties names to validate.
        :return: Id of a resource.
        """

        resource_id = None
        if resource:
            for prop, allowed_states in zip(props, allowed_vals):
                if resource[prop] not in allowed_states:
                    msg = _("Originating %(res)s %(prop)s must be one of "
                            "'%(vals)s' values")
                    msg = msg % {'res': resource_name,
                                 'prop': prop,
                                 'vals': ', '.join(allowed_states)}
                    # TODO(harlowja): what happens if the status changes after
                    # this initial resource status check occurs??? Seems like
                    # someone could delete the resource after this check passes
                    # but before the volume is officially created?
                    raise exc(reason=msg)
                resource_id = resource['id']
        return resource_id

    def _extract_consistencygroup(self, consistencygroup):
        return self._extract_resource(consistencygroup, (CG_PROCEED_STATUS,),
                                      exception.InvalidConsistencyGroup,
                                      'consistencygroup')

    def _extract_group(self, group):
        return self._extract_resource(group, (GROUP_PROCEED_STATUS,),
                                      exception.InvalidGroup,
                                      'group')

    def _extract_cgsnapshot(self, cgsnapshot):
        return self._extract_resource(cgsnapshot, (CGSNAPSHOT_PROCEED_STATUS,),
                                      exception.InvalidCgSnapshot,
                                      'CGSNAPSHOT')

    def _extract_snapshot(self, snapshot):
        return self._extract_resource(snapshot, (SNAPSHOT_PROCEED_STATUS,),
                                      exception.InvalidSnapshot, 'snapshot')

    def _extract_source_volume(self, source_volume):
        return self._extract_resource(source_volume, (SRC_VOL_PROCEED_STATUS,),
                                      exception.InvalidVolume, 'source volume')

    def _extract_source_replica(self, source_replica):
        return self._extract_resource(source_replica, (SRC_VOL_PROCEED_STATUS,
                                                       REPLICA_PROCEED_STATUS),
                                      exception.InvalidVolume,
                                      'replica', ('status',
                                                  'replication_status'))

    @staticmethod
    def _extract_size(size, source_volume, snapshot):
        """Extracts and validates the volume size.

        This function will validate or when not provided fill in the provided
        size variable from the source_volume or snapshot and then does
        validation on the size that is found and returns said validated size.
        """

        def validate_snap_size(size):
            if snapshot and size < snapshot.volume_size:
                msg = _("Volume size '%(size)s'GB cannot be smaller than"
                        " the snapshot size %(snap_size)sGB. "
                        "They must be >= original snapshot size.")
                msg = msg % {'size': size,
                             'snap_size': snapshot.volume_size}
                raise exception.InvalidInput(reason=msg)

        def validate_source_size(size):
            if source_volume and size < source_volume['size']:
                msg = _("Volume size '%(size)s'GB cannot be smaller than "
                        "original volume size  %(source_size)sGB. "
                        "They must be >= original volume size.")
                msg = msg % {'size': size,
                             'source_size': source_volume['size']}
                raise exception.InvalidInput(reason=msg)

        def validate_int(size):
            if not isinstance(size, six.integer_types) or size <= 0:
                msg = _("Volume size '%(size)s' must be an integer and"
                        " greater than 0") % {'size': size}
                raise exception.InvalidInput(reason=msg)

        # Figure out which validation functions we should be applying
        # on the size value that we extract.
        validator_functors = [validate_int]
        if source_volume:
            validator_functors.append(validate_source_size)
        elif snapshot:
            validator_functors.append(validate_snap_size)

        # If the size is not provided then try to provide it.
        if not size and source_volume:
            size = source_volume['size']
        elif not size and snapshot:
            size = snapshot.volume_size

        size = utils.as_int(size)
        LOG.debug("Validating volume '%(size)s' using %(functors)s" %
                  {'size': size,
                   'functors': ", ".join([common.make_pretty_name(func)
                                          for func in validator_functors])})
        for func in validator_functors:
            func(size)
        return size

    def _check_image_metadata(self, context, image_id, size):
        """Checks image existence and validates that the image metadata."""

        # Check image existence
        if image_id is None:
            return

        # NOTE(harlowja): this should raise an error if the image does not
        # exist, this is expected as it signals that the image_id is missing.
        image_meta = self.image_service.show(context, image_id)

        # check whether image is active
        if image_meta['status'] != 'active':
            msg = _('Image %(image_id)s is not active.')\
                % {'image_id': image_id}
            raise exception.InvalidInput(reason=msg)

        # Check image size is not larger than volume size.
        image_size = utils.as_int(image_meta['size'], quiet=False)
        image_size_in_gb = (image_size + GB - 1) // GB
        if image_size_in_gb > size:
            msg = _('Size of specified image %(image_size)sGB'
                    ' is larger than volume size %(volume_size)sGB.')
            msg = msg % {'image_size': image_size_in_gb, 'volume_size': size}
            raise exception.InvalidInput(reason=msg)

        # Check image min_disk requirement is met for the particular volume
        min_disk = image_meta.get('min_disk', 0)
        if size < min_disk:
            msg = _('Volume size %(volume_size)sGB cannot be smaller'
                    ' than the image minDisk size %(min_disk)sGB.')
            msg = msg % {'volume_size': size, 'min_disk': min_disk}
            raise exception.InvalidInput(reason=msg)

    def _get_image_volume_type(self, context, image_id):
        """Get cinder_img_volume_type property from the image metadata."""

        # Check image existence
        if image_id is None:
            return None

        image_meta = self.image_service.show(context, image_id)

        # check whether image is active
        if image_meta['status'] != 'active':
            msg = (_('Image %(image_id)s is not active.') %
                   {'image_id': image_id})
            raise exception.InvalidInput(reason=msg)

        # Retrieve 'cinder_img_volume_type' property from glance image
        # metadata.
        image_volume_type = "cinder_img_volume_type"
        properties = image_meta.get('properties')
        if properties:
            try:
                img_vol_type = properties.get(image_volume_type)
                if img_vol_type is None:
                    return None
                volume_type = volume_types.get_volume_type_by_name(
                    context,
                    img_vol_type)
            except exception.VolumeTypeNotFoundByName:
                LOG.warning(_LW("Failed to retrieve volume_type from image "
                                "metadata. '%(img_vol_type)s' doesn't match "
                                "any volume types."),
                            {'img_vol_type': img_vol_type})
                return None

            LOG.debug("Retrieved volume_type from glance image metadata. "
                      "image_id: %(image_id)s, "
                      "image property: %(image_volume_type)s, "
                      "volume_type: %(volume_type)s." %
                      {'image_id': image_id,
                       'image_volume_type': image_volume_type,
                       'volume_type': volume_type})
            return volume_type

    def _extract_availability_zone(self, availability_zone, snapshot,
                                   source_volume, group):
        """Extracts and returns a validated availability zone.

        This function will extract the availability zone (if not provided) from
        the snapshot or source_volume and then performs a set of validation
        checks on the provided or extracted availability zone and then returns
        the validated availability zone.
        """

        # If the volume will be created in a group, it should be placed in
        # in same availability zone as the group.
        if group:
            try:
                availability_zone = group['availability_zone']
            except (TypeError, KeyError):
                pass

        # Try to extract the availability zone from the corresponding snapshot
        # or source volume if either is valid so that we can be in the same
        # availability zone as the source.
        if availability_zone is None:
            if snapshot:
                try:
                    availability_zone = snapshot['volume']['availability_zone']
                except (TypeError, KeyError):
                    pass
            if source_volume and availability_zone is None:
                try:
                    availability_zone = source_volume['availability_zone']
                except (TypeError, KeyError):
                    pass

        if availability_zone is None:
            if CONF.default_availability_zone:
                availability_zone = CONF.default_availability_zone
            else:
                # For backwards compatibility use the storage_availability_zone
                availability_zone = CONF.storage_availability_zone

        if availability_zone not in self.availability_zones:
            if CONF.allow_availability_zone_fallback:
                original_az = availability_zone
                availability_zone = (
                    CONF.default_availability_zone or
                    CONF.storage_availability_zone)
                LOG.warning(_LW("Availability zone '%(s_az)s' "
                                "not found, falling back to "
                                "'%(s_fallback_az)s'."),
                            {'s_az': original_az,
                             's_fallback_az': availability_zone})
            else:
                msg = _("Availability zone '%(s_az)s' is invalid.")
                msg = msg % {'s_az': availability_zone}
                raise exception.InvalidInput(reason=msg)

        # If the configuration only allows cloning to the same availability
        # zone then we need to enforce that.
        if CONF.cloned_volume_same_az:
            snap_az = None
            try:
                snap_az = snapshot['volume']['availability_zone']
            except (TypeError, KeyError):
                pass
            if snap_az and snap_az != availability_zone:
                msg = _("Volume must be in the same "
                        "availability zone as the snapshot")
                raise exception.InvalidInput(reason=msg)
            source_vol_az = None
            try:
                source_vol_az = source_volume['availability_zone']
            except (TypeError, KeyError):
                pass
            if source_vol_az and source_vol_az != availability_zone:
                msg = _("Volume must be in the same "
                        "availability zone as the source volume")
                raise exception.InvalidInput(reason=msg)

        return availability_zone

    def _get_encryption_key_id(self, key_manager, context, volume_type_id,
                               snapshot, source_volume):
        encryption_key_id = None
        if volume_types.is_encrypted(context, volume_type_id):
            if snapshot is not None:  # creating from snapshot
                encryption_key_id = snapshot['encryption_key_id']
            elif source_volume is not None:  # cloning volume
                encryption_key_id = source_volume['encryption_key_id']

            # NOTE(joel-coffman): References to the encryption key should *not*
            # be copied because the key is deleted when the volume is deleted.
            # Clone the existing key and associate a separate -- but
            # identical -- key with each volume.
            if encryption_key_id is not None:
                encryption_key_id = key_manager.store(
                    context, key_manager.get(context, encryption_key_id))
            else:
                volume_type_encryption = (
                    volume_types.get_volume_type_encryption(context,
                                                            volume_type_id))