#    Copyright 2019 NTT DATA.
#
#    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_log import log as logging
from oslo_utils import uuidutils
from oslo_versionedobjects import base as ovoo_base
from sqlalchemy.orm import joinedload

from tacker._i18n import _
from tacker.common import exceptions
from tacker.common import utils
from tacker.db import api as db_api
from tacker.db.db_sqlalchemy import api
from tacker.db.db_sqlalchemy import models
from tacker.objects import base
from tacker.objects import fields


VNF_SOFTWARE_IMAGE_OPTIONAL_ATTRS = ['metadata']

LOG = logging.getLogger(__name__)


def _metadata_add_to_db(context, id, metadata, max_retries=10):
    for attempt in range(max_retries):
        with db_api.context_manager.writer.using(context):

            new_entries = []
            for key, value in metadata.items():
                new_entries.append({"key": key,
                                    "value": value,
                                    "image_uuid": id})
            if new_entries:
                context.session.execute(
                    models.VnfSoftwareImageMetadata.__table__.insert(),
                    new_entries)

            return metadata


@db_api.context_manager.writer
def _vnf_sw_image_create(context, values, metadata=None):
    vnf_sw_image = models.VnfSoftwareImage()

    vnf_sw_image.update(values)
    vnf_sw_image.save(context.session)
    vnf_sw_image._metadata = []

    if metadata:
        _metadata_add_to_db(context, vnf_sw_image.id, metadata)
        context.session.expire(vnf_sw_image, ['_metadata'])
        vnf_sw_image._metadata

    return vnf_sw_image


@db_api.context_manager.reader
def _vnf_sw_image_get_by_id(context, id):

    query = api.model_query(context, models.VnfSoftwareImage,
        read_deleted="no").filter_by(id=id).options(
            joinedload(models.VnfSoftwareImage._metadata))

    result = query.first()

    if not result:
        raise exceptions.VnfSoftwareImageNotFound(id=id)

    return result


@base.TackerObjectRegistry.register
class VnfSoftwareImage(base.TackerObject, base.TackerPersistentObject):

    ALL_ATTRIBUTES = {
        "softwareImages": {
            'id': ('software_image_id', 'uuid', 'VnfSoftwareImage'),
            'imagePath': ('image_path', 'string', 'VnfSoftwareImage'),
            'diskFormat': ('disk_format', 'string', 'VnfSoftwareImage'),
            'userMetadata/*': ('metadata', 'key_value_pair',
            {"key_column": "key", "value_column": "value",
             "model": "VnfSoftwareImageMetadata"}),
            'size': ('size', 'number', 'VnfSoftwareImage'),
            'createdAt': ('created_at', 'datetime', 'VnfSoftwareImage'),
            'name': ('name', 'string', 'VnfSoftwareImage'),
            'minDisk': ('min_disk', 'number', 'VnfSoftwareImage'),
            'version': ('version', 'string', 'VnfSoftwareImage'),
            'provider': ('provider', 'string', 'VnfSoftwareImage'),
            'minRam': ('min_ram', 'number', 'VnfSoftwareImage'),
            'containerFormat': ('container_format', 'string',
                                'VnfSoftwareImage'),
            "checksum": {
                'hash': ('hash', 'string', 'VnfSoftwareImage'),
                'algorithm': ('algorithm', 'string', 'VnfSoftwareImage')
            }
        }
    }

    FLATTEN_ATTRIBUTES = utils.flatten_dict(ALL_ATTRIBUTES.copy())
    SIMPLE_ATTRIBUTES = ['id', 'imagePath', 'diskFormat', 'size',
            'createdAt', 'name', 'minDisk', 'version', 'provider', 'minRam',
            'containerFormat']
    COMPLEX_ATTRIBUTES = ['softwareImages', 'softwareImages/userMetadata',
        'softwareImages/checksum']

    # Version 1.0: Initial version
    VERSION = '1.0'

    fields = {
        'id': fields.UUIDField(nullable=False),
        'software_image_id': fields.StringField(nullable=False),
        'flavour_uuid': fields.UUIDField(nullable=False),
        'name': fields.StringField(nullable=True),
        'provider': fields.StringField(nullable=True),
        'version': fields.StringField(nullable=True),
        'algorithm': fields.StringField(nullable=True),
        'hash': fields.StringField(nullable=True),
        'container_format': fields.StringField(nullable=True),
        'disk_format': fields.StringField(nullable=True),
        'min_disk': fields.IntegerField(),
        'min_ram': fields.IntegerField(default=0),
        'size': fields.IntegerField(),
        'image_path': fields.StringField(),
        'metadata': fields.DictOfStringsField(nullable=True)
    }

    @staticmethod
    def _from_db_object(context, vnf_sw_image, db_sw_image,
                        expected_attrs=None):

        vnf_sw_image._context = context
        for key in vnf_sw_image.fields:
            if key in VNF_SOFTWARE_IMAGE_OPTIONAL_ATTRS:
                continue
            else:
                db_key = key

            setattr(vnf_sw_image, key, db_sw_image[db_key])

        vnf_sw_image._extra_attributes_from_db_object(vnf_sw_image,
                          db_sw_image, expected_attrs)

        vnf_sw_image.obj_reset_changes()

        return vnf_sw_image

    @staticmethod
    def _extra_attributes_from_db_object(vnf_sw_image, db_sw_image,
                                         expected_attrs=None):
        """Method to help with migration of extra attributes to objects.

        """
        if expected_attrs is None:
            expected_attrs = []

        if 'metadata' in expected_attrs:
            setattr(vnf_sw_image, 'metadata', db_sw_image['metadetails'])

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

        LOG.debug("Lazy-loading '%(attr)s' on %(name)s id %(id)s",
                  {'attr': attrname,
                   'name': self.obj_name(),
                   'id': self.id,
                   })

        self._obj_load_attr(attrname)

    def _obj_load_attr(self, attrname):
        """Internal method for loading attributes from vnf flavour."""

        if attrname in self.fields and attrname != 'id':
            self._load_generic(attrname)
        else:
            # NOTE(nirajsingh): Raise error if non existing field is
            # requested.
            raise exceptions.ObjectActionError(
                action='obj_load_attr',
                reason=_('attribute %s not lazy-loadable') % attrname)

        self.obj_reset_changes([attrname])

    def _load_generic(self, attrname):
        software_image = self.__class__.get_by_id(self._context,
                                              id=self.id,
                                              expected_attrs=attrname)
        if attrname not in software_image:
            raise exceptions.ObjectActionError(
                action='obj_load_attr',
                reason=_('loading %s requires recursion') % attrname)

        for field in self.fields:
            if field in software_image and field not in self:
                setattr(self, field, getattr(software_image, field))

    @base.remotable
    def create(self):
        if self.obj_attr_is_set('id'):
            raise exceptions.ObjectActionError(action='create',
                                               reason=_('already created'))
        updates = self.obj_get_changes()

        if 'id' not in updates:
            updates['id'] = uuidutils.generate_uuid()
            self.id = updates['id']

        metadata = updates.pop('metadata', None)
        db_sw_image = _vnf_sw_image_create(self._context, updates,
                                           metadata=metadata)
        self._from_db_object(self._context, self, db_sw_image)

    @base.remotable_classmethod
    def get_by_id(cls, context, id, expected_attrs=None):
        db_sw_image = _vnf_sw_image_get_by_id(context, id)
        return cls._from_db_object(context, cls(), db_sw_image,
                                   expected_attrs=expected_attrs)

    def _get_user_metadata(self, include_fields=None):
        # Need special handling for field containing key-value pair.
        # If user requests softwareImages/userMetadata/key1 and if
        # softwareImages/userMetadata contains key1=value1, key2=value2,
        # it should return only keys that are requested in include_fields.
        # If user requests only softwareImages/userMetadata, then in that
        # case, it should return all key/value pairs. If any of the requested
        # key is not present, then it will siliently ignore it.
        key = 'softwareImages/userMetadata'
        if key in include_fields or '%s/*' % key in \
                include_fields:
            return self.metadata
        else:
            # Check if user has requested specified keys from
            # softwareImages/userMetadata.
            key_list = []
            special_key = '%s/' % key
            for field in include_fields:
                if field.startswith(special_key):
                    key_list.append(field[len(special_key):])

            data_resp = dict()
            for key_req in key_list:
                if key_req in self.metadata:
                    data_resp[key_req] = self.metadata[key_req]

            if len(key_list) > 0:
                return data_resp

    def to_dict(self, include_fields=None):
        response = dict()
        fields = ['softwareImages/%s' % attribute for attribute in
            self.SIMPLE_ATTRIBUTES]
        to_fields = set(fields).intersection(include_fields)
        for field in to_fields:
            display_field = field.split("/")[-1]
            response[display_field] = getattr(self,
                    self.FLATTEN_ATTRIBUTES[field][0])

        # add checksum
        to_fields = set([key for key in self.FLATTEN_ATTRIBUTES.keys()
            if key.startswith('softwareImages/checksum')])
        checksum = dict()
        to_fields = to_fields.intersection(include_fields)
        for field in to_fields:
            display_field = field.split("/")[-1]
            checksum[display_field] = getattr(self,
                    self.FLATTEN_ATTRIBUTES[field][0])

        if checksum:
            response.update({"checksum": checksum})

        user_metadata = self._get_user_metadata(include_fields)
        if user_metadata is not None:
            response.update({"userMetadata": user_metadata})

        return response


@base.TackerObjectRegistry.register
class VnfSoftwareImagesList(ovoo_base.ObjectListBase, base.TackerObject):

    VERSION = '1.0'

    fields = {
        'objects': fields.ListOfObjectsField('VnfSoftwareImage')
    }
