# Copyright 2013: Mirantis Inc.
# 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.

"""Defines interface for DB access.

The underlying driver is loaded as a :class:`LazyPluggable`.

Functions in this module are imported into the rally.common.db namespace.
Call these functions from rally.common.db namespace, not the
rally.common.db.api namespace.

All functions in this module return objects that implement a dictionary-like
interface. Currently, many of these objects are sqlalchemy objects that
implement a dictionary interface. However, a future goal is to have all of
these objects be simple dictionaries.


**Related Flags**

:backend:  string to lookup in the list of LazyPluggable backends.
           `sqlalchemy` is the only supported backend right now.

:connection:  string specifying the sqlalchemy connection to use, like:
              `sqlite:///var/lib/cinder/cinder.sqlite`.

:enable_new_services:  when adding a new service to the database, is it in the
                       pool of available hardware (Default: True)

"""

import datetime as dt
import functools
import tempfile
import time

from oslo_db import exception as db_exc
from oslo_db import options as db_options
from oslo_db.sqlalchemy import session as db_session
import sqlalchemy as sa
import sqlalchemy.orm   # noqa

from rally.common import cfg
from rally.common.db import models
from rally import consts
from rally import exceptions
from rally.task.processing import charts


CONF = cfg.CONF

db_options.set_defaults(
    CONF, connection="sqlite:///%s/rally.sqlite" % tempfile.gettempdir())

_FACADE = None
_SESSION_MAKER = None


def _create_facade_lazily():
    global _FACADE

    if _FACADE is None:
        _FACADE = db_session.EngineFacade.from_config(CONF)

    return _FACADE


def get_engine():
    facade = _create_facade_lazily()
    return facade.get_engine()


def get_session():
    global _SESSION_MAKER

    if not _SESSION_MAKER:
        _SESSION_MAKER = sa.orm.sessionmaker()
        _SESSION_MAKER.configure(bind=get_engine())

    return _SESSION_MAKER()


def engine_reset():
    global _FACADE, _SESSION_MAKER
    _FACADE = None
    _SESSION_MAKER = None


def serialize(data):
    if data is None:
        return None
    if isinstance(data, (int,
                         str,
                         dt.date,
                         dt.time,
                         float,
                         )):
        return data
    if isinstance(data, models.RallyBase):
        result = data.as_dict()
        for k in result:
            result[k] = serialize(result[k])
        return result
    if isinstance(data, (list, tuple)):
        return [serialize(d) for d in data]
    if isinstance(data, dict):
        result = {}
        for k in data:
            result[k] = serialize(data[k])
        return result

    raise ValueError("Failed to serialize %r data type." % type(data).__name__)


def with_session(f):

    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        session = get_session()
        session.expire_on_commit = False
        try:
            result = f(session, *args, **kwargs)
            session.commit()
        except Exception:
            session.rollback()
            raise
        finally:
            session.close()

        return serialize(result)

    return wrapper


@with_session
def tags_get(session, uuid, tag_type):
    query = session.query(models.Tag.tag).filter_by(uuid=uuid, type=tag_type)
    return [t.tag for t in query.distinct().all()]


def _uuids_by_tags_get(session, tag_type, tags):
    tags = (session.query(models.Tag.uuid)
                   .filter(models.Tag.type == tag_type,
                           models.Tag.tag.in_(tags)).distinct())
    return [t.uuid for t in tags.all()]


def _task_workload_data_get_all(session, workload_uuid):
    results = (session.query(models.WorkloadData)
                      .filter_by(workload_uuid=workload_uuid)
                      .order_by(models.WorkloadData.chunk_order.asc()))

    return sorted([raw for workload_data in results
                   for raw in workload_data.chunk_data["raw"]],
                  key=lambda x: x["timestamp"])


def _subtasks_get_all_by_task_uuid(session, task_uuid):
    result = session.query(models.Subtask).filter_by(task_uuid=task_uuid).all()
    subtasks = []
    for subtask in result:
        subtask = subtask.as_dict()
        subtask["workloads"] = []
        workloads = (session.query(models.Workload).
                     filter_by(subtask_uuid=subtask["uuid"]).all())
        for workload in workloads:
            workload = workload.as_dict()
            workload["data"] = _task_workload_data_get_all(
                session, workload["uuid"])
            subtask["workloads"].append(workload)
        subtasks.append(subtask)
    return subtasks


@with_session
def task_get(session, uuid=None, detailed=False):

    task = session.query(models.Task).filter_by(uuid=uuid).first()
    if not task:
        raise exceptions.DBRecordNotFound(
            criteria="uuid: %s" % uuid, table="tasks")
    task = task.as_dict()
    task["tags"] = sorted(tags_get(uuid, consts.TagType.TASK))

    if detailed:
        task["subtasks"] = _subtasks_get_all_by_task_uuid(session, uuid)

    return task


@with_session
def task_get_status(session, uuid=None):
    task = (session.query(models.Task)
                   .options(sa.orm.load_only("status"))
                   .filter_by(uuid=uuid).first())
    if not task:
        raise exceptions.DBRecordNotFound(
            criteria="uuid: %s" % uuid, table="tasks")

    return task.status


@with_session
def task_create(session, values):
    tags = values.pop("tags", [])
    # TODO(ikhudoshyn): currently 'input_task'
    # does not come in 'values'
    # After completely switching to the new
    # DB schema in API we should reconstruct
    # input_task's from associated workloads
    # the same is true for 'pass_sla',
    # 'task_duration', 'validation_result'
    # and 'validation_duration'
    task = models.Task(**values)
    session.add(task)
    session.commit()
    task = task.as_dict()

    if tags:
        session.bulk_save_objects(
            [models.Tag(uuid=task["uuid"], tag=t,