# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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.
"""
Implementation of SQLAlchemy backend.
"""

import datetime
import warnings

from nova import db
from nova import exception
from nova import flags
from nova import utils
from nova.db.sqlalchemy import models
from nova.db.sqlalchemy.session import get_session
from sqlalchemy import or_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql import exists
from sqlalchemy.sql import func
from sqlalchemy.sql.expression import literal_column

FLAGS = flags.FLAGS


def is_admin_context(context):
    """Indicates if the request context is an administrator."""
    if not context:
        warnings.warn(_('Use of empty request context is deprecated'),
                      DeprecationWarning)
        raise Exception('die')
    return context.is_admin


def is_user_context(context):
    """Indicates if the request context is a normal user."""
    if not context:
        return False
    if context.is_admin:
        return False
    if not context.user_id or not context.project_id:
        return False
    return True


def authorize_project_context(context, project_id):
    """Ensures that the request context has permission to access the
       given project.
    """
    if is_user_context(context):
        if not context.project:
            raise exception.NotAuthorized()
        elif context.project_id != project_id:
            raise exception.NotAuthorized()


def authorize_user_context(context, user_id):
    """Ensures that the request context has permission to access the
       given user.
    """
    if is_user_context(context):
        if not context.user:
            raise exception.NotAuthorized()
        elif context.user_id != user_id:
            raise exception.NotAuthorized()


def can_read_deleted(context):
    """Indicates if the context has access to deleted objects."""
    if not context:
        return False
    return context.read_deleted


def require_admin_context(f):
    """Decorator used to indicate that the method requires an
       administrator context.
    """
    def wrapper(*args, **kwargs):
        if not is_admin_context(args[0]):
            raise exception.AdminRequired()
        return f(*args, **kwargs)
    return wrapper


def require_context(f):
    """Decorator used to indicate that the method requires either
       an administrator or normal user context.
    """
    def wrapper(*args, **kwargs):
        if not is_admin_context(args[0]) and not is_user_context(args[0]):
            raise exception.AdminRequired()
        return f(*args, **kwargs)
    return wrapper


###################

@require_admin_context
def service_destroy(context, service_id):
    session = get_session()
    with session.begin():
        service_ref = service_get(context, service_id, session=session)
        service_ref.delete(session=session)

        if service_ref.topic == 'compute' and \
            len(service_ref.compute_node) != 0:
            for c in service_ref.compute_node:
                c.delete(session=session)


@require_admin_context
def service_get(context, service_id, session=None):
    if not session:
        session = get_session()

    result = session.query(models.Service).\
                     options(joinedload('compute_node')).\
                     filter_by(id=service_id).\
                     filter_by(deleted=can_read_deleted(context)).\
                     first()

    if not result:
        raise exception.ServiceNotFound(service_id=service_id)

    return result


@require_admin_context
def service_get_all(context, disabled=None):
    session = get_session()
    query = session.query(models.Service).\
                   filter_by(deleted=can_read_deleted(context))

    if disabled is not None:
        query = query.filter_by(disabled=disabled)

    return query.all()


@require_admin_context
def service_get_all_by_topic(context, topic):
    session = get_session()
    return session.query(models.Service).\
                   filter_by(deleted=False).\
                   filter_by(disabled=False).\
                   filter_by(topic=topic).\
                   all()


@require_admin_context
def service_get_by_host_and_topic(context, host, topic):
    session = get_session()
    return session.query(models.Service).\
                   filter_by(deleted=False).\
                   filter_by(disabled=False).\
                   filter_by(host=host).\
                   filter_by(topic=topic).\
                   first()


@require_admin_context
def service_get_all_by_host(context, host):
    session = get_session()
    return session.query(models.Service).\
                   filter_by(deleted=False).\
                   filter_by(host=host).\
                   all()


@require_admin_context
def service_get_all_compute_by_host(context, host):
    topic = 'compute'
    session = get_session()
    result = session.query(models.Service).\
                  options(joinedload('compute_node')).\
                  filter_by(deleted=False).\
                  filter_by(host=host).\
                  filter_by(topic=topic).\
                  all()

    if not result:
        raise exception.ComputeHostNotFound(host=host)

    return result


@require_admin_context
def _service_get_all_topic_subquery(context, session, topic, subq, label):
    sort_value = getattr(subq.c, label)
    return session.query(models.Service, func.coalesce(sort_value, 0)).\
                   filter_by(topic=topic).\
                   filter_by(deleted=False).\
                   filter_by(disabled=False).\
                   outerjoin((subq, models.Service.host == subq.c.host)).\
                   order_by(sort_value).\
                   all()


@require_admin_context
def service_get_all_compute_sorted(context):
    session = get_session()
    with session.begin():
        # NOTE(vish): The intended query is below
        #             SELECT services.*, COALESCE(inst_cores.instance_cores,
        #                                         0)
        #             FROM services LEFT OUTER JOIN
        #             (SELECT host, SUM(instances.vcpus) AS instance_cores
        #              FROM instances GROUP BY host) AS inst_cores
        #             ON services.host = inst_cores.host
        topic = 'compute'
        label = 'instance_cores'
        subq = session.query(models.Instance.host,
                             func.sum(models.Instance.vcpus).label(label)).\
                       filter_by(deleted=False).\
                       group_by(models.Instance.host).\
                       subquery()
        return _service_get_all_topic_subquery(context,
                                               session,
                                               topic,
                                               subq,
                                               label)


@require_admin_context
def service_get_all_network_sorted(context):
    session = get_session()
    with session.begin():
        topic = 'network'
        label = 'network_count'
        subq = session.query(models.Network.host,
                             func.count(models.Network.id).label(label)).\
                       filter_by(deleted=False).\
                       group_by(models.Network.host).\
                       subquery()
        return _service_get_all_topic_subquery(context,
                                               session,
                                               topic,
                                               subq,
                                               label)


@require_admin_context
def service_get_all_volume_sorted(context):
    session = get_session()
    with session.begin():
        topic = 'volume'
        label = 'volume_gigabytes'
        subq = session.query(models.Volume.host,
                             func.sum(models.Volume.size).label(label)).\
                       filter_by(deleted=False).\
                       group_by(models.Volume.host).\
                       subquery()
        return _service_get_all_topic_subquery(context,
                                               session,
                                               topic,
                                               subq,
                                               label)


@require_admin_context
def service_get_by_args(context, host, binary):
    session = get_session()
    result = session.query(models.Service).\
                     filter_by(host=host).\
                     filter_by(binary=binary).\
                     filter_by(deleted=can_read_deleted(context)).\
                     first()
    if not result:
        raise exception.HostBinaryNotFound(host=host, binary=binary)

    return result


@require_admin_context
def service_create(context, values):
    service_ref = models.Service()
    service_ref.update(values)
    if not FLAGS.enable_new_services:
        service_ref.disabled = True
    service_ref.save()
    return service_ref


@require_admin_context
def service_update(context, service_id, values):
    session = get_session()
    with session.begin():
        service_ref = service_get(context, service_id, session=session)
        service_ref.update(values)
        service_ref.save(session=session)


###################


@require_admin_context
def compute_node_get(context, compute_id, session=None):
    if not session:
        session = get_session()

    result = session.query(models.ComputeNode).\
                     filter_by(id=compute_id).\
                     filter_by(deleted=can_read_deleted(context)).\
                     first()

    if not result:
        raise exception.ComputeHostNotFound(host=compute_id)

    return result


@require_admin_context
def compute_node_create(context, values):
    compute_node_ref = models.ComputeNode()
    compute_node_ref.update(values)
    compute_node_ref.save()
    return compute_node_ref


@require_admin_context
def compute_node_update(context, compute_id, values):
    session = get_session()
    with session.begin():
        compute_ref = compute_node_get(context, compute_id, session=session)
        compute_ref.update(values)
        compute_ref.save(session=session)


###################


@require_admin_context
def certificate_get(context, certificate_id, session=None):
    if not session:
        session = get_session()

    result = session.query(models.Certificate).\
                     filter_by(id=certificate_id).\
                     filter_by(deleted=can_read_deleted(context)).\
                     first()

    if not result:
        raise exception.CertificateNotFound(certificate_id=certificate_id)

    return result


@require_admin_context
def certificate_create(context, values):
    certificate_ref = models.Certificate()
    for (key, value) in values.iteritems():
        certificate_ref[key] = value
    certificate_ref.save()
    return certificate_ref


@require_admin_context
def certificate_destroy(context, certificate_id):
    session = get_session()
    with session.begin():
        certificate_ref = certificate_get(context,
                                          certificate_id,
                                          session=session)
        certificate_ref.delete(session=session)


@require_admin_context
def certificate_get_all_by_project(context, project_id):
    session = get_session()
    return session.query(models.Certificate).\
                   filter_by(project_id=project_id).\
                   filter_by(deleted=False).\
                   all()


@require_admin_context
def certificate_get_all_by_user(context, user_id):
    session = get_session()
    return session.query(models.Certificate).\
                   filter_by(user_id=user_id).\
                   filter_by(deleted=False).\
                   all()


@require_admin_context
def certificate_get_all_by_user_and_project(_context, user_id, project_id):
    session = get_session()
    return session.query(models.Certificate).\
                   filter_by(user_id=user_id).\
                   filter_by(project_id=project_id).\
                   filter_by(deleted=False).\
                   all()


@require_admin_context
def certificate_update(context, certificate_id, values):
    session = get_session()
    with session.begin():