#    Copyright 2013 Rackspace Hosting.
#
#    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 collections

from oslo_db import exception as db_exc

from nova.db.api import api as api_db_api
from nova.db.api import models as api_models
from nova.db.main import api as main_db_api
from nova.db.main import models as main_models
from nova.db import utils as db_utils
from nova import exception
from nova.objects import base
from nova.objects import fields
from nova import quota


def ids_from_instance(context, instance):
    if (context.is_admin and
            context.project_id != instance['project_id']):
        project_id = instance['project_id']
    else:
        project_id = context.project_id
    if context.user_id != instance['user_id']:
        user_id = instance['user_id']
    else:
        user_id = context.user_id
    return project_id, user_id


# TODO(lyj): This method needs to be cleaned up once the
# ids_from_instance helper method is renamed or some common
# method is added for objects.quotas.
def ids_from_security_group(context, security_group):
    return ids_from_instance(context, security_group)


# TODO(PhilD): This method needs to be cleaned up once the
# ids_from_instance helper method is renamed or some common
# method is added for objects.quotas.
def ids_from_server_group(context, server_group):
    return ids_from_instance(context, server_group)


@base.NovaObjectRegistry.register
class Quotas(base.NovaObject):
    # Version 1.0: initial version
    # Version 1.1: Added create_limit() and update_limit()
    # Version 1.2: Added limit_check() and count()
    # Version 1.3: Added check_deltas(), limit_check_project_and_user(),
    #              and count_as_dict()
    VERSION = '1.3'

    fields = {
        # TODO(melwitt): Remove this field in version 2.0 of the object.
        'reservations': fields.ListOfStringsField(nullable=True,
                                                  default=[]),
        'project_id': fields.StringField(nullable=True,
                                         default=None),
        'user_id': fields.StringField(nullable=True,
                                      default=None),
    }

    def obj_load_attr(self, attr):
        self.obj_set_defaults(attr)
        # NOTE(danms): This is strange because resetting these would cause
        # them not to be saved to the database. I would imagine this is
        # from overzealous defaulting and that all three fields ultimately
        # get set all the time. However, quotas are weird, so replicate the
        # longstanding behavior of setting defaults and clearing their
        # dirty bit.
        self.obj_reset_changes(fields=[attr])

    @staticmethod
    @api_db_api.context_manager.reader
    def _get_from_db(context, project_id, resource, user_id=None):
        model = api_models.ProjectUserQuota if user_id else api_models.Quota
        query = context.session.query(model).\
                        filter_by(project_id=project_id).\
                        filter_by(resource=resource)
        if user_id:
            query = query.filter_by(user_id=user_id)
        result = query.first()
        if not result:
            if user_id:
                raise exception.ProjectUserQuotaNotFound(project_id=project_id,
                                                         user_id=user_id)
            else:
                raise exception.ProjectQuotaNotFound(project_id=project_id)
        return result

    @staticmethod
    @api_db_api.context_manager.reader
    def _get_all_from_db(context, project_id):
        return context.session.query(api_models.ProjectUserQuota).\
                        filter_by(project_id=project_id).\
                        all()

    @staticmethod
    @api_db_api.context_manager.reader
    def _get_all_from_db_by_project(context, project_id):
        # by_project refers to the returned dict that has a 'project_id' key
        rows = context.session.query(api_models.Quota).\
                        filter_by(project_id=project_id).\
                        all()
        result = {'project_id': project_id}
        for row in rows:
            result[row.resource] = row.hard_limit
        return result

    @staticmethod
    @api_db_api.context_manager.reader
    def _get_all_from_db_by_project_and_user(context, project_id, user_id):
        # by_project_and_user refers to the returned dict that has
        # 'project_id' and 'user_id' keys
        columns = (api_models.ProjectUserQuota.resource,
                   api_models.ProjectUserQuota.hard_limit)
        user_quotas = context.session.query(*columns).\
                        filter_by(project_id=project_id).\
                        filter_by(user_id=user_id).\
                        all()
        result = {'project_id': project_id, 'user_id': user_id}
        for user_quota in user_quotas:
            result[user_quota.resource] = user_quota.hard_limit
        return result

    @staticmethod
    @api_db_api.context_manager.writer
    def _destroy_all_in_db_by_project(context, project_id):
        per_project = context.session.query(api_models.Quota).\
                            filter_by(project_id=project_id).\
                            delete(synchronize_session=False)
        per_user = context.session.query(api_models.ProjectUserQuota).\
                            filter_by(project_id=project_id).\
                            delete(synchronize_session=False)
        if not per_project and not per_user:
            raise exception.ProjectQuotaNotFound(project_id=project_id)

    @staticmethod
    @api_db_api.context_manager.writer
    def _destroy_all_in_db_by_project_and_user(context, project_id, user_id):
        result = context.session.query(api_models.ProjectUserQuota).\
                        filter_by(project_id=project_id).\
                        filter_by(user_id=user_id).\
                        delete(synchronize_session=False)
        if not result:
            raise exception.ProjectUserQuotaNotFound(project_id=project_id,
                                                     user_id=user_id)

    @staticmethod
    @api_db_api.context_manager.reader
    def _get_class_from_db(context, class_name, resource):
        result = context.session.query(api_models.QuotaClass).\
                        filter_by(class_name=class_name).\
                        filter_by(resource=resource).\
                        first()
        if not result:
            raise exception.QuotaClassNotFound(class_name=class_name)
        return result

    @staticmethod
    @api_db_api.context_manager.reader
    def _get_all_class_from_db_by_name(context, class_name):
        # by_name refers to the returned dict that has a 'class_name' key
        rows = context.session.query(api_models.QuotaClass).\
                        filter_by(class_name=class_name).\
                        all()
        result = {'class_name': class_name}
        for row in rows:
            result[row.resource] = row.hard_limit
        return result

    @staticmethod
    @api_db_api.context_manager.writer
    def _create_limit_in_db(context, project_id, resource, limit,
                            user_id=None):
        # TODO(melwitt): We won't have per project resources after nova-network
        # is removed.
        # TODO(stephenfin): We need to do something here now...but what?
        per_user = (
            user_id and
            resource not in main_db_api.quota_get_per_project_resources()
        )
        quota_ref = (api_models.ProjectUserQuota() if per_user
                     else api_models.Quota())
        if per_user:
            quota_ref.user_id = user_id
        quota_ref.project_id = project_id
        quota_ref.resource = resource
        quota_ref.hard_limit = limit
        try:
            quota_ref.save(context.session)
        except db_exc.DBDuplicateEntry:
            raise exception.QuotaExists(project_id=project_id,
                                        resource=resource)
        return quota_ref

    @staticmethod
    @api_db_api.context_manager.writer
    def _update_limit_in_db(context, project_id, resource, limit,
                            user_id=None):
        # TODO(melwitt): We won't have per project resources after nova-network
        # is removed.
        # TODO(stephenfin): We need to do something here now...but what?
        per_user = (
            user_id and
            resource not in main_db_api.quota_get_per_project_resources()
        )
        model = api_models.ProjectUserQuota if per_user else api_models.Quota
        query = context.session.query(model).\
                        filter_by(project_id=project_id).\
                        filter_by(resource=resource)
        if per_user:
            query = query.filter_by(user_id=user_id)

        result = query.update({'hard_limit': limit})
        if not result:
            if per_user:
                raise exception.ProjectUserQuotaNotFound(project_id=project_id,
                                                         user_id=user_id)
            else:
                raise exception.ProjectQuotaNotFound(project_id=project_id)

    @staticmethod
    @api_db_api.context_manager.writer
    def _create_class_in_db(context, class_name, resource, limit):
        # NOTE(melwitt): There's no unique constraint on the QuotaClass model,
        # so check for duplicate manually.
        try:
            Quotas._get_class_from_db(context, class_name, resource)
        except exception.QuotaClassNotFound:
            pass
        else:
            raise exception.QuotaClassExists(class_name=class_name,
                                             resource=resource)
        quota_class_ref = api_models.QuotaClass()
        quota_class_ref.class_name = class_name
        quota_class_ref.resource = resource
        quota_class_ref.hard_limit = limit
        quota_class_ref.save(context.session)
        return quota_class_ref

    @staticmethod
    @api_db_api.context_manager.writer
    def _update_class_in_db(context, class_name, resource, limit):
        result = context.session.query(api_models.QuotaClass).\
                        filter_by(class_name=class_name).\
                        filter_by(resource=resource).\
                        update({'hard_limit': limit})
        if not result:
            raise exception.QuotaClassNotFound(class_name=class_name)

    # TODO(melwitt): Remove this method in version 2.0 of the object.
    @base.remotable
    def reserve(self, expire=None, project_id=None, user_id=None,
                **deltas):
        # Honor the expected attributes even though we're not reserving
        # anything anymore. This will protect against things exploding if
        # someone has an Ocata compute host running by accident, for example.
        self.reservations = None
        self.project_id = project_id
        self.user_id = user_id
        self.obj_reset_changes()

    # TODO(melwitt): Remove this method in version 2.0 of the object.
    @base.remotable
    def commit(self):
        pass

    # TODO(melwitt): Remove this method in version 2.0 of the object.
    @base.remotable
    def rollback(self):
        pass

    @base.remotable_classmethod
    def limit_check(cls, context, project_id=None, user_id=None, **values):
        """Check quota limits."""
        return quota.QUOTAS.limit_check(
            context, project_id=project_id, user_id=user_id, **values)

    @base.remotable_classmethod
    def limit_check_project_and_user(cls, context, project_values=None,
                                     user_values=None, project_id=None,
                                     user_id=None):
        """Check values against quota limits."""
        return quota.QUOTAS.limit_check_project_and_user(context,
            project_values=project_values, user_values=user_values,
            project_id=project_id, user_id=user_id)

    # NOTE(melwitt): This can be removed once no old code can call count().
    @base.remotable_classmethod
    def count(cls, context, resource, *args, **kwargs):
        """Count a resource."""
        count = quota.QUOTAS.count_as_dict(context, resource, *args, **kwargs)
        key = 'user' if 'user' in count else 'project'
        return count[key][resource]

    @base.remotable_classmethod
    def count_as_dict(cls, context, resource, *args, **kwargs):
        """Count a resource and return a dict."""
        return quota.QUOTAS.count_as_dict(
            context, resource, *args, **kwargs)

    @base.remotable_classmethod
    def check_deltas(cls, context, deltas, *count_args, **count_kwargs):
        """Check usage delta against quota limits.

        This does a Quotas.count_as_dict() followed by a
        Quotas.limit_check_project_and_user() using the provided deltas.

        :param context: The request context, for access checks
        :param deltas: A dict of {resource_name: delta, ...} to check against
                       the quota limits
        :param count_args: Optional positional arguments to pass to
                           count_as_dict()
        :param count_kwargs: Optional keyword arguments to pass to
                             count_as_dict()
        :param check_project_id: Optional project_id for scoping the limit
                                 check to a different project than in the
                                 context
        :param check_user_id: Optional user_id for scoping the limit check to a
                              different user than in the context
        :raises: exception.OverQuota if the limit check exceeds the quota
                 limits
        """
        # We can't do f(*args, kw=None, **kwargs) in python 2.x
        check_project_id = count_kwargs.pop('check_project_id', None)
        check_user_id = count_kwargs.pop('check_user_id', None)

        check_kwargs = collections.defaultdict(dict)
        for resource in deltas:
            # If we already counted a resource in a batch count, avoid
            # unnecessary re-counting and avoid creating empty dicts in
            # the defaultdict.
            if (resource in check_kwargs.get('project_values', {}) or
                    resource in check_kwargs.get('user_values', {})):
                continue
            count = cls.count_as_dict(context, resource, *count_args,
                                      **count_kwargs)
            for res in count.get('project', {}):
                if res in deltas:
                    total = count['project'][res] + deltas[res]
                    check_kwargs['project_values'][res] = total
            for res in count.get('user', {}):
                if res in deltas:
                    total = count['user'][res] + deltas[res]
                    check_kwargs['user_values'][res] = total
        if check_project_id is not None:
            check_kwargs['project_id'] = check_project_id
        if check_user_id is not None:
            check_kwargs['user_id'] = check_user_id
        try:
            cls.limit_check_project_and_user(context, **check_kwargs)
        except exception.OverQuota as exc:
            # Report usage in the exception when going over quota
            key = 'user' if 'user' in count else 'project'
            exc.kwargs['usages'] = count[key]
            raise exc

    @base.remotable_classmethod
    def create_limit(cls, context, project_id, resource, limit, user_id=None):
        try:
            main_db_api.quota_get(
                context, project_id, resource, user_id=user_id)
        except exception.QuotaNotFound:
            cls._create_limit_in_db(context, project_id, resource, limit,
                                    user_id=user_id)
        else:
            raise exception.QuotaExists(project_id=project_id,
                                        resource=resource)

    @base.remotable_classmethod
    def update_limit(cls, context, project_id, resource, limit, user_id=None):
        try:
            cls._update_limit_in_db(context, project_id, resource, limit,
                                    user_id=user_id)
        except exception.QuotaNotFound:
            main_db_api.quota_update(context, project_id, resource, limit,
                            user_id=user_id)

    @classmethod
    def create_class(cls, context, class_name, resource, limit):
        try:
            main_db_api.quota_class_get(context, class_name, resource)
        except exception.QuotaClassNotFound:
            cls._create_class_in_db(context, class_name, resource, limit)
        else:
            raise exception.QuotaClassExists(class_name=class_name,
                                             resource=resource)

    @classmethod
    def update_class(cls, context, class_name, resource, limit):
        try:
            cls._update_class_in_db(context, class_name, resource, limit)
        except exception.QuotaClassNotFound:
            main_db_api.quota_class_update(
                context, class_name, resource, limit)

    # NOTE(melwitt): The following methods are not remotable and return
    # dict-like database model objects. We are using classmethods to provide
    # a common interface for accessing the api/main databases.
    @classmethod
    def get(cls, context, project_id, resource, user_id=None):
        try:
            quota = cls._get_from_db(context, project_id, resource,
                                     user_id=user_id)
        except exception.QuotaNotFound:
            quota = main_db_api.quota_get(context, project_id, resource,
                                 user_id=user_id)
        return quota

    @classmethod
    def get_all(cls, context, project_id):
        api_db_quotas = cls._get_all_from_db(context, project_id)
        main_db_quotas = main_db_api.quota_get_all(context, project_id)
        return api_db_quotas + main_db_quotas

    @classmethod
    def get_all_by_project(cls, context, project_id):
        api_db_quotas_dict = cls._get_all_from_db_by_project(context,
                                                             project_id)
        main_db_quotas_dict = main_db_api.quota_get_all_by_project(
            context, project_id)
        for k, v in api_db_quotas_dict.items():
            main_db_quotas_dict[k] = v
        return main_db_quotas_dict

    @classmethod
    def get_all_by_project_and_user(cls, context, project_id, user_id):
        api_db_quotas_dict = cls._get_all_from_db_by_project_and_user(
                context, project_id, user_id)
        main_db_quotas_dict = main_db_api.quota_get_all_by_project_and_user(
                context, project_id, user_id)
        for k, v in api_db_quotas_dict.items():
            main_db_quotas_dict[k] = v
        return main_db_quotas_dict

    @classmethod
    def destroy_all_by_project(cls, context, project_id):
        try:
            cls._destroy_all_in_db_by_project(context, project_id)
        except exception.ProjectQuotaNotFound:
            main_db_api.quota_destroy_all_by_project(context, project_id)

    @classmethod
    def destroy_all_by_project_and_user(cls, context, project_id, user_id):
        try:
            cls._destroy_all_in_db_by_project_and_user(context, project_id,
                                                       user_id)
        except exception.ProjectUserQuotaNotFound:
            main_db_api.quota_destroy_all_by_project_and_user(
                context, project_id, user_id)

    @classmethod
    def get_class(cls, context, class_name, resource):
        try:
            qclass = cls._get_class_from_db(context, class_name, resource)
        except exception.QuotaClassNotFound:
            qclass = main_db_api.quota_class_get(context, class_name, resource)
        return qclass

    @classmethod
    def get_default_class(cls, context):
        try:
            qclass = cls._get_all_class_from_db_by_name(
                    context, main_db_api._DEFAULT_QUOTA_NAME)
        except exception.QuotaClassNotFound:
            qclass = main_db_api.quota_class_get_default(context)
        return qclass

    @classmethod
    def get_all_class_by_name(cls, context, class_name):
        api_db_quotas_dict = cls._get_all_class_from_db_by_name(context,
                                                                class_name)
        main_db_quotas_dict = main_db_api.quota_class_get_all_by_name(context,
                                                             class_name)
        for k, v in api_db_quotas_dict.items():
            main_db_quotas_dict[k] = v
        return main_db_quotas_dict


@base.NovaObjectRegistry.register
class QuotasNoOp(Quotas):
    # TODO(melwitt): Remove this method in version 2.0 of the object.
    def reserve(context, expire=None, project_id=None, user_id=None,
                **deltas):
        pass

    # TODO(melwitt): Remove this method in version 2.0 of the object.
    def commit(self, context=None):
        pass

    # TODO(melwitt): Remove this method in version 2.0 of the object.
    def rollback(self, context=None):
        pass

    def check_deltas(cls, context, deltas, *count_args, **count_kwargs):
        pass


@db_utils.require_context
@main_db_api.pick_context_manager_reader
def _get_main_per_project_limits(context, limit):
    return context.session.query(main_models.Quota).\
        filter_by(deleted=0).\
        limit(limit).\
        all()


@db_utils.require_context
@main_db_api.pick_context_manager_reader
def _get_main_per_user_limits(context, limit):
    return context.session.query(main_models.ProjectUserQuota).\
        filter_by(deleted=0).\
        limit(limit).\
        all()


@db_utils.require_context
@main_db_api.pick_context_manager_writer
def _destroy_main_per_project_limits(context, project_id, resource):
    context.session.query(main_models.Quota).\
        filter_by(deleted=0).\
        filter_by(project_id=project_id).\
        filter_by(resource=resource).\
        soft_delete(synchronize_session=False)


@db_utils.require_context
@main_db_api.pick_context_manager_writer
def _destroy_main_per_user_limits(context, project_id, resource, user_id):
    context.session.query(main_models.ProjectUserQuota).\
        filter_by(deleted=0).\
        filter_by(project_id=project_id).\
        filter_by(user_id=user_id).\
        filter_by(resource=resource).\
        soft_delete(synchronize_session=False)


@api_db_api.context_manager.writer
def _create_limits_in_api_db(context, db_limits, per_user=False):
    for db_limit in db_limits:
        user_id = db_limit.user_id if per_user else None
        Quotas._create_limit_in_db(context, db_limit.project_id,
                                   db_limit.resource, db_limit.hard_limit,
                                   user_id=user_id)


def migrate_quota_limits_to_api_db(context, max_count):
    # Migrate per project limits
    main_per_project_limits = _get_main_per_project_limits(context, max_count)
    done = 0
    try:
        # Create all the limits in a single transaction.
        _create_limits_in_api_db(context, main_per_project_limits)
    except exception.QuotaExists:
        # NOTE(melwitt): This can happen if the migration is interrupted after
        # limits were created in the api db but before they were deleted from
        # the main db, and the migration is re-run.
        pass
    # Delete the limits separately.
    for db_limit in main_per_project_limits:
        _destroy_main_per_project_limits(context, db_limit.project_id,
                                         db_limit.resource)
        done += 1
    if done == max_count:
        return len(main_per_project_limits), done
    # Migrate per user limits
    max_count -= done
    main_per_user_limits = _get_main_per_user_limits(context, max_count)
    try:
        # Create all the limits in a single transaction.
        _create_limits_in_api_db(context, main_per_user_limits, per_user=True)
    except exception.QuotaExists:
        # NOTE(melwitt): This can happen if the migration is interrupted after
        # limits were created in the api db but before they were deleted from
        # the main db, and the migration is re-run.
        pass
    # Delete the limits separately.
    for db_limit in main_per_user_limits:
        _destroy_main_per_user_limits(context, db_limit.project_id,
                                      db_limit.resource, db_limit.user_id)
        done += 1
    return len(main_per_project_limits) + len(main_per_user_limits), done


@db_utils.require_context
@main_db_api.pick_context_manager_reader
def _get_main_quota_classes(context, limit):
    return context.session.query(main_models.QuotaClass).\
        filter_by(deleted=0).\
        limit(limit).\
        all()


@main_db_api.pick_context_manager_writer
def _destroy_main_quota_classes(context, db_classes):
    for db_class in db_classes:
        context.session.query(main_models.QuotaClass).\
            filter_by(deleted=0).\
            filter_by(id=db_class.id).\
            soft_delete(synchronize_session=False)


@api_db_api.context_manager.writer
def _create_classes_in_api_db(context, db_classes):
    for db_class in db_classes:
        Quotas._create_class_in_db(context, db_class.class_name,
                                   db_class.resource, db_class.hard_limit)


def migrate_quota_classes_to_api_db(context, max_count):
    main_quota_classes = _get_main_quota_classes(context, max_count)
    done = 0
    try:
        # Create all the classes in a single transaction.
        _create_classes_in_api_db(context, main_quota_classes)
    except exception.QuotaClassExists:
        # NOTE(melwitt): This can happen if the migration is interrupted after
        # classes were created in the api db but before they were deleted from
        # the main db, and the migration is re-run.
        pass
    # Delete the classes in a single transaction.
    _destroy_main_quota_classes(context, main_quota_classes)
    found = done = len(main_quota_classes)
    return found, done
