#    Copyright 2015 SimpliVity Corp.
#
#    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_serialization import jsonutils
from oslo_versionedobjects import fields

from cinder import db
from cinder import exception
from cinder.i18n import _
from cinder import objects
from cinder.objects import base
from cinder.objects import fields as c_fields


@base.CinderObjectRegistry.register
class VolumeAttachment(base.CinderPersistentObject, base.CinderObject,
                       base.CinderObjectDictCompat,
                       base.CinderComparableObject):
    # Version 1.0: Initial version
    # Version 1.1: Added volume relationship
    # Version 1.2: Added connection_info attribute
    # Version 1.3: Added the connector attribute.
    VERSION = '1.3'

    OPTIONAL_FIELDS = ['volume']
    obj_extra_fields = ['project_id', 'volume_host']

    # NOTE: When adding a field obj_make_compatible needs to be updated
    fields = {
        'id': fields.UUIDField(),
        'volume_id': fields.UUIDField(),
        'instance_uuid': fields.UUIDField(nullable=True),
        'attached_host': fields.StringField(nullable=True),
        'mountpoint': fields.StringField(nullable=True),

        'attach_time': fields.DateTimeField(nullable=True),
        'detach_time': fields.DateTimeField(nullable=True),

        'attach_status': c_fields.VolumeAttachStatusField(nullable=True),
        'attach_mode': fields.StringField(nullable=True),

        'volume': fields.ObjectField('Volume', nullable=False),
        'connection_info': c_fields.DictOfNullableField(nullable=True),
        'connector': c_fields.DictOfNullableField(nullable=True)
    }

    @property
    def project_id(self):
        return self.volume.project_id

    @property
    def volume_host(self):
        return self.volume.host

    @classmethod
    def _get_expected_attrs(cls, context, *args, **kwargs):
        return ['volume']

    @classmethod
    def _from_db_object(cls, context, attachment, db_attachment,
                        expected_attrs=None):
        if expected_attrs is None:
            expected_attrs = cls._get_expected_attrs(context)

        for name, field in attachment.fields.items():
            if name in cls.OPTIONAL_FIELDS:
                continue
            value = db_attachment.get(name)
            if isinstance(field, fields.IntegerField):
                value = value or 0
            if name in ('connection_info', 'connector'):
                # Both of these fields are nullable serialized json dicts.
                setattr(attachment, name,
                        jsonutils.loads(value) if value else None)
            else:
                attachment[name] = value
        # NOTE: Check against the ORM instance's dictionary instead of using
        # hasattr or get to avoid the lazy loading of the Volume on
        # VolumeList.get_all.
        # Getting a Volume loads its VolumeAttachmentList, which think they
        # have the volume loaded, but they don't.  More detail on
        # https://review.opendev.org/632549
        # and its related bug report.
        if 'volume' in expected_attrs and 'volume' in vars(db_attachment):
            db_volume = db_attachment.volume
            if db_volume:
                attachment.volume = objects.Volume._from_db_object(
                    context, objects.Volume(), db_volume)

        attachment._context = context
        attachment.obj_reset_changes()

        # This is an online data migration which we should remove when enough
        # time has passed and we have a blocker schema migration to check to
        # make sure that the attachment_specs table is empty. Operators should
        # run the "cinder-manage db online_data_migrations" CLI to force the
        # migration on-demand.
        connector = db.attachment_specs_get(context, attachment.id)
        if connector:
            # Update ourselves and delete the attachment_specs.
            attachment.connector = connector
            attachment.save()
            # TODO(mriedem): Really need a delete-all method for this.
            for spec_key in connector:
                db.attachment_specs_delete(
                    context, attachment.id, spec_key)

        return attachment

    def obj_load_attr(self, attrname):
        if attrname not in self.OPTIONAL_FIELDS:
            raise exception.ObjectActionError(
                action='obj_load_attr',
                reason=_('attribute %s not lazy-loadable') % attrname)
        if not self._context:
            raise exception.OrphanedObjectError(method='obj_load_attr',
                                                objtype=self.obj_name())

        if attrname == 'volume':
            volume = objects.Volume.get_by_id(self._context, self.volume_id)
            self.volume = volume

        self.obj_reset_changes(fields=[attrname])

    @staticmethod
    def _convert_connection_info_to_db_format(updates):
        properties = updates.pop('connection_info', None)
        if properties is not None:
            updates['connection_info'] = jsonutils.dumps(properties)

    @staticmethod
    def _convert_connector_to_db_format(updates):
        connector = updates.pop('connector', None)
        if connector is not None:
            updates['connector'] = jsonutils.dumps(connector)

    def save(self):
        updates = self.cinder_obj_get_changes()
        if updates:
            if 'connection_info' in updates:
                self._convert_connection_info_to_db_format(updates)
            if 'connector' in updates:
                self._convert_connector_to_db_format(updates)
            if 'volume' in updates:
                raise exception.ObjectActionError(action='save',
                                                  reason=_('volume changed'))

            db.volume_attachment_update(self._context, self.id, updates)
            self.obj_reset_changes()

    def finish_attach(self, instance_uuid, host_name,
                      mount_point, attach_mode='rw'):
        with self.obj_as_admin():
            db_volume, updated_values = db.volume_attached(
                self._context, self.id,
                instance_uuid, host_name,
                mount_point, attach_mode)
        self.update(updated_values)
        self.obj_reset_changes(updated_values.keys())
        return objects.Volume._from_db_object(self._context,
                                              objects.Volume(),
                                              db_volume)

    def create(self):
        if self.obj_attr_is_set('id'):
            raise exception.ObjectActionError(action='create',
                                              reason=_('already created'))
        updates = self.cinder_obj_get_changes()
        if 'connector' in updates:
            self._convert_connector_to_db_format(updates)
        with self.obj_as_admin():
            db_attachment = db.volume_attach(self._context, updates)
        self._from_db_object(self._context, self, db_attachment)

    def destroy(self):
        updated_values = db.attachment_destroy(self._context, self.id)
        self.update(updated_values)
        self.obj_reset_changes(updated_values.keys())


@base.CinderObjectRegistry.register
class VolumeAttachmentList(base.ObjectListBase, base.CinderObject):
    # Version 1.0: Initial version
    # Version 1.1: Remove volume_id in get_by_host|instance
    VERSION = '1.1'

    fields = {
        'objects': fields.ListOfObjectsField('VolumeAttachment'),
    }

    @classmethod
    def get_all_by_volume_id(cls, context, volume_id):
        attachments = db.volume_attachment_get_all_by_volume_id(context,
                                                                volume_id)
        return base.obj_make_list(context,
                                  cls(context),
                                  objects.VolumeAttachment,
                                  attachments)

    @classmethod
    def get_all_by_host(cls, context, host, search_opts=None):
        attachments = db.volume_attachment_get_all_by_host(context,
                                                           host,
                                                           search_opts)
        return base.obj_make_list(context, cls(context),
                                  objects.VolumeAttachment, attachments)

    @classmethod
    def get_all_by_instance_uuid(cls, context,
                                 instance_uuid, search_opts=None):
        attachments = db.volume_attachment_get_all_by_instance_uuid(
            context, instance_uuid, search_opts)
        return base.obj_make_list(context, cls(context),
                                  objects.VolumeAttachment, attachments)

    @classmethod
    def get_all(cls, context, search_opts=None,
                marker=None, limit=None, offset=None,
                sort_keys=None, sort_direction=None):
        attachments = db.volume_attachment_get_all(
            context, search_opts, marker, limit, offset, sort_keys,
            sort_direction)
        return base.obj_make_list(context, cls(context),
                                  objects.VolumeAttachment, attachments)

    @classmethod
    def get_all_by_project(cls, context, project_id, search_opts=None,
                           marker=None, limit=None, offset=None,
                           sort_keys=None, sort_direction=None):
        attachments = db.volume_attachment_get_all_by_project(
            context, project_id, search_opts, marker, limit, offset, sort_keys,
            sort_direction)
        return base.obj_make_list(context, cls(context),
                                  objects.VolumeAttachment, attachments)
