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

import copy

from oslo_db import exception as db_exc
from oslo_utils import uuidutils
from oslo_utils import versionutils
from sqlalchemy.orm import contains_eager
from sqlalchemy.orm import joinedload

from nova.compute import utils as compute_utils
from nova import db
from nova.db.sqlalchemy import api as db_api
from nova.db.sqlalchemy import api_models
from nova.db.sqlalchemy import models as main_models
from nova import exception
from nova import objects
from nova.objects import base
from nova.objects import fields


LAZY_LOAD_FIELDS = ['hosts']


def _instance_group_get_query(context, id_field=None, id=None):
    query = context.session.query(api_models.InstanceGroup).\
            options(joinedload('_policies')).\
            options(joinedload('_members'))
    if not context.is_admin:
        query = query.filter_by(project_id=context.project_id)
    if id and id_field:
        query = query.filter(id_field == id)
    return query


def _instance_group_model_get_query(context, model_class, group_id):
    return context.session.query(model_class).filter_by(group_id=group_id)


def _instance_group_model_add(context, model_class, items, item_models, field,
                              group_id, append_to_models=None):
    models = []
    already_existing = set()
    for db_item in item_models:
        already_existing.add(getattr(db_item, field))
        models.append(db_item)
    for item in items:
        if item in already_existing:
            continue
        model = model_class()
        values = {'group_id': group_id}
        values[field] = item
        model.update(values)
        context.session.add(model)
        if append_to_models:
            append_to_models.append(model)
        models.append(model)
    return models


def _instance_group_policies_add(context, group, policies):
    query = _instance_group_model_get_query(context,
                                            api_models.InstanceGroupPolicy,
                                            group.id)
    query = query.filter(
                api_models.InstanceGroupPolicy.policy.in_(set(policies)))
    return _instance_group_model_add(context, api_models.InstanceGroupPolicy,
                                     policies, query.all(), 'policy', group.id,
                                     append_to_models=group._policies)


def _instance_group_members_add(context, group, members):
    query = _instance_group_model_get_query(context,
                                            api_models.InstanceGroupMember,
                                            group.id)
    query = query.filter(
                api_models.InstanceGroupMember.instance_uuid.in_(set(members)))
    return _instance_group_model_add(context, api_models.InstanceGroupMember,
                                     members, query.all(), 'instance_uuid',
                                     group.id, append_to_models=group._members)


def _instance_group_members_add_by_uuid(context, group_uuid, members):
    # NOTE(melwitt): The condition on the join limits the number of members
    # returned to only those we wish to check as already existing.
    group = context.session.query(api_models.InstanceGroup).\
            outerjoin(api_models.InstanceGroupMember,
            api_models.InstanceGroupMember.instance_uuid.in_(set(members))).\
            filter(api_models.InstanceGroup.uuid == group_uuid).\
            options(contains_eager('_members')).first()
    if not group:
        raise exception.InstanceGroupNotFound(group_uuid=group_uuid)
    return _instance_group_model_add(context, api_models.InstanceGroupMember,
                                     members, group._members, 'instance_uuid',
                                     group.id)


# TODO(berrange): Remove NovaObjectDictCompat
@base.NovaObjectRegistry.register
class InstanceGroup(base.NovaPersistentObject, base.NovaObject,
                    base.NovaObjectDictCompat):
    # Version 1.0: Initial version
    # Version 1.1: String attributes updated to support unicode
    # Version 1.2: Use list/dict helpers for policies, metadetails, members
    # Version 1.3: Make uuid a non-None real string
    # Version 1.4: Add add_members()
    # Version 1.5: Add get_hosts()
    # Version 1.6: Add get_by_name()
    # Version 1.7: Deprecate metadetails
    # Version 1.8: Add count_members_by_user()
    # Version 1.9: Add get_by_instance_uuid()
    # Version 1.10: Add hosts field
    VERSION = '1.10'

    fields = {
        'id': fields.IntegerField(),

        'user_id': fields.StringField(nullable=True),
        'project_id': fields.StringField(nullable=True),

        'uuid': fields.UUIDField(),
        'name': fields.StringField(nullable=True),

        'policies': fields.ListOfStringsField(nullable=True),
        'members': fields.ListOfStringsField(nullable=True),
        'hosts': fields.ListOfStringsField(nullable=True),
        }

    def obj_make_compatible(self, primitive, target_version):
        target_version = versionutils.convert_version_to_tuple(target_version)
        if target_version < (1, 7):
            # NOTE(danms): Before 1.7, we had an always-empty
            # metadetails property
            primitive['metadetails'] = {}

    @staticmethod
    def _from_db_object(context, instance_group, db_inst):
        """Method to help with migration to objects.

        Converts a database entity to a formal object.
        """
        # Most of the field names match right now, so be quick
        for field in instance_group.fields:
            if field in LAZY_LOAD_FIELDS:
                continue
            # This is needed to handle db models from both the api
            # database and the main database. In the migration to
            # the api database, we have removed soft-delete, so
            # the object fields for delete must be filled in with
            # default values for db models from the api database.
            ignore = {'deleted': False,
                      'deleted_at': None}
            if field in ignore and not hasattr(db_inst, field):
                instance_group[field] = ignore[field]
            else:
                instance_group[field] = db_inst[field]

        instance_group._context = context
        instance_group.obj_reset_changes()
        return instance_group

    @staticmethod
    @db_api.api_context_manager.reader
    def _get_from_db_by_uuid(context, uuid):
        grp = _instance_group_get_query(context,
                                        id_field=api_models.InstanceGroup.uuid,
                                        id=uuid).first()
        if not grp:
            raise exception.InstanceGroupNotFound(group_uuid=uuid)
        return grp

    @staticmethod
    @db_api.api_context_manager.reader
    def _get_from_db_by_id(context, id):
        grp = _instance_group_get_query(context,
                                        id_field=api_models.InstanceGroup.id,
                                        id=id).first()
        if not grp:
            raise exception.InstanceGroupNotFound(group_uuid=id)