# Copyright (c) 2015 OpenStack Foundation.  All rights reserved.
#
#    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
import datetime

import sqlalchemy as sa
from sqlalchemy.orm import exc as orm_exc
from sqlalchemy import sql

from neutron.db import api as db_api
from neutron.db import common_db_mixin as common_db_api
from neutron.db.quota import models as quota_models


# Wrapper for utcnow - needed for mocking it in unit tests
def utcnow():
    return datetime.datetime.utcnow()


class QuotaUsageInfo(collections.namedtuple(
    'QuotaUsageInfo', ['resource', 'tenant_id', 'used', 'dirty'])):
    """Information about resource quota usage."""


class ReservationInfo(collections.namedtuple(
    'ReservationInfo', ['reservation_id', 'tenant_id',
                        'expiration', 'deltas'])):
    """Information about a resource reservation."""


def get_quota_usage_by_resource_and_tenant(context, resource, tenant_id,
                                           lock_for_update=False):
    """Return usage info for a given resource and tenant.

    :param context: Request context
    :param resource: Name of the resource
    :param tenant_id: Tenant identifier
    :param lock_for_update: if True sets a write-intent lock on the query
    :returns: a QuotaUsageInfo instance
    """

    query = common_db_api.model_query(context, quota_models.QuotaUsage)
    query = query.filter_by(resource=resource, tenant_id=tenant_id)

    if lock_for_update:
        query = query.with_lockmode('update')

    result = query.first()
    if not result:
        return
    return QuotaUsageInfo(result.resource,
                          result.tenant_id,
                          result.in_use,
                          result.dirty)


def get_quota_usage_by_resource(context, resource):
    query = common_db_api.model_query(context, quota_models.QuotaUsage)
    query = query.filter_by(resource=resource)
    return [QuotaUsageInfo(item.resource,
                           item.tenant_id,
                           item.in_use,
                           item.dirty) for item in query]


def get_quota_usage_by_tenant_id(context, tenant_id):
    query = common_db_api.model_query(context, quota_models.QuotaUsage)
    query = query.filter_by(tenant_id=tenant_id)
    return [QuotaUsageInfo(item.resource,
                           item.tenant_id,
                           item.in_use,
                           item.dirty) for item in query]


def set_quota_usage(context, resource, tenant_id,
                    in_use=None, delta=False):
    """Set resource quota usage.

    :param context: instance of neutron context with db session
    :param resource: name of the resource for which usage is being set
    :param tenant_id: identifier of the tenant for which quota usage is
                      being set
    :param in_use: integer specifying the new quantity of used resources,
                   or a delta to apply to current used resource
    :param delta: Specifies whether in_use is an absolute number
                  or a delta (default to False)
    """
    with db_api.autonested_transaction(context.session):
        query = common_db_api.model_query(context, quota_models.QuotaUsage)
        query = query.filter_by(resource=resource).filter_by(
            tenant_id=tenant_id)
        usage_data = query.first()
        if not usage_data:
            # Must create entry
            usage_data = quota_models.QuotaUsage(
                resource=resource,
                tenant_id=tenant_id)
            context.session.add(usage_data)
        # Perform explicit comparison with None as 0 is a valid value
        if in_use is not None:
            if delta:
                in_use = usage_data.in_use + in_use
            usage_data.in_use = in_use
        # After an explicit update the dirty bit should always be reset
        usage_data.dirty = False
    return QuotaUsageInfo(usage_data.resource,
                          usage_data.tenant_id,
                          usage_data.in_use,
                          usage_data.dirty)


def set_quota_usage_dirty(context, resource, tenant_id, dirty=True):
    """Set quota usage dirty bit for a given resource and tenant.

    :param resource: a resource for which quota usage if tracked
    :param tenant_id: tenant identifier
    :param dirty: the desired value for the dirty bit (defaults to True)
    :returns: 1 if the quota usage data were updated, 0 otherwise.
    """
    query = common_db_api.model_query(context, quota_models.QuotaUsage)
    query = query.filter_by(resource=resource).filter_by(tenant_id=tenant_id)
    return query.update({'dirty': dirty})


def set_resources_quota_usage_dirty(context, resources, tenant_id, dirty=True):
    """Set quota usage dirty bit for a given tenant and multiple resources.

    :param resources: list of resource for which the dirty bit is going
                      to be set
    :param tenant_id: tenant identifier
    :param dirty: the desired value for the dirty bit (defaults to True)
    :returns: the number of records for which the bit was actually set.
    """
    query = common_db_api.model_query(context, quota_models.QuotaUsage)
    query = query.filter_by(tenant_id=tenant_id)
    if resources:
        query = query.filter(quota_models.QuotaUsage.resource.in_(resources))
    # synchronize_session=False needed because of the IN condition
    return query.update({'dirty': dirty}, synchronize_session=False)


def set_all_quota_usage_dirty(context, resource, dirty=True):
    """Set the dirty bit on quota usage for all tenants.

    :param resource: the resource for which the dirty bit should be set
    :returns: the number of tenants for which the dirty bit was
              actually updated
    """
    query = common_db_api.model_query(context, quota_models.QuotaUsage)
    query = query.filter_by(resource=resource)
    return query.update({'dirty': dirty})


def create_reservation(context, tenant_id, deltas, expiration=None):
    # This method is usually called from within another transaction.
    # Consider using begin_nested
    with context.session.begin(subtransactions=True):
        expiration = expiration or (utcnow() + datetime.timedelta(0, 120))
        resv = quota_models.Reservation(tenant_id=tenant_id,
                                        expiration=expiration)
        context.session.add(resv)
        for (resource, delta) in deltas.items():
            context.session.add(
                quota_models.ResourceDelta(resource=resource,
                                           amount=delta,
                                           reservation=resv))
    return ReservationInfo(resv['id'],
                           resv['tenant_id'],
                           resv['expiration'],
                           dict((delta.resource, delta.amount)
                                for delta in resv.resource_deltas))


def get_reservation(context, reservation_id):
    query = context.session.query(quota_models.Reservation).filter_by(
        id=reservation_id)
    resv = query.first()
    if not resv:
        return
    return ReservationInfo(resv['id'],
                           resv['tenant_id'],
                           resv['expiration'],
                           dict((delta.resource, delta.amount)
                                for delta in resv.resource_deltas))


def remove_reservation(context, reservation_id, set_dirty=False):
    delete_query = context.session.query(quota_models.Reservation).filter_by(
        id=reservation_id)
    # Not handling MultipleResultsFound as the query is filtering by primary
    # key
    try:
        reservation = delete_query.one()
    except orm_exc.NoResultFound:
        # TODO(salv-orlando): Raise here and then handle the exception?
        return
    tenant_id = reservation.tenant_id
    resources = [delta.resource for delta in reservation.resource_deltas]
    num_deleted = delete_query.delete()
    if set_dirty:
        # quota_usage for all resource involved in this reservation must
        # be marked as dirty
        set_resources_quota_usage_dirty(context, resources, tenant_id)
    return num_deleted


def get_reservations_for_resources(context, tenant_id, resources,
                                   expired=False):
    """Retrieve total amount of reservations for specified resources.

    :param context: Neutron context with db session
    :param tenant_id: Tenant identifier
    :param resources: Resources for which reserved amounts should be fetched
    :param expired: False to fetch active reservations, True to fetch expired
                    reservations (defaults to False)
    :returns: a dictionary mapping resources with corresponding deltas
    """
    if not resources:
        # Do not waste time
        return
    now = utcnow()
    resv_query = context.session.query(
        quota_models.ResourceDelta.resource,
        quota_models.Reservation.expiration,
        sql.func.sum(quota_models.ResourceDelta.amount)).join(
        quota_models.Reservation)
    if expired:
        exp_expr = (quota_models.Reservation.expiration < now)
    else:
        exp_expr = (quota_models.Reservation.expiration >= now)
    resv_query = resv_query.filter(sa.and_(
        quota_models.Reservation.tenant_id == tenant_id,
        quota_models.ResourceDelta.resource.in_(resources),
        exp_expr)).group_by(
        quota_models.ResourceDelta.resource,
        quota_models.Reservation.expiration)
    return dict((resource, total_reserved)
            for (resource, exp, total_reserved) in resv_query)


def remove_expired_reservations(context, tenant_id=None):
    now = utcnow()
    resv_query = context.session.query(quota_models.Reservation)
    if tenant_id:
        tenant_expr = (quota_models.Reservation.tenant_id == tenant_id)
    else:
        tenant_expr = sql.true()
    resv_query = resv_query.filter(sa.and_(
        tenant_expr, quota_models.Reservation.expiration < now))
    return resv_query.delete()
