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

from oslo_config import cfg
from oslo_db import api as db_api
from oslo_db import exception as db_exc
from oslo_db.sqlalchemy import enginefacade
from oslo_db.sqlalchemy import utils as sqlalchemyutils
from oslo_log import log
from oslo_utils import timeutils
from sqlalchemy import or_
import uuid

from freezer_api.api.common import utils as json_utils
from freezer_api.common._i18n import _
from freezer_api.common.check import check_client_capabilities
from freezer_api.common import elasticv2_utils as utilsv2
from freezer_api.common import exceptions as freezer_api_exc
from freezer_api.common.json_schemas import SUPPORTED_ACTIONS
from freezer_api.common.json_schemas import SUPPORTED_ENGINES
from freezer_api.common.json_schemas import SUPPORTED_MODES
from freezer_api.common.json_schemas import SUPPORTED_STORAGES
from freezer_api.db.sqlalchemy import models


CONF = cfg.CONF
LOG = log.getLogger(__name__)


main_context_lock = threading.Lock()
main_context_manager = None
main_context = None


def _get_main_context():
    global main_context
    with main_context_lock:
        if main_context is None:
            main_context = threading.local()
    return main_context


def _get_main_context_manager():
    global main_context_manager

    with main_context_lock:
        if not main_context_manager:
            main_context_manager = enginefacade.transaction_context()
            # NOTE(noonedeadpunk): Disable foreign key enforcement by default
            # to avoid breaking tests that have incomplete data setup.
            main_context_manager.configure(sqlite_fk=False)

    return main_context_manager


def _context_manager_from_context(context):
    if context:
        try:
            return context.db_connection
        except AttributeError:
            pass


def session_for_read():
    reader = _get_main_context_manager().reader
    return reader.using(_get_main_context())


def session_for_write():
    writer = _get_main_context_manager().writer
    return writer.using(_get_main_context())


def clear_db_env():
    """
    Unset global configuration variables for database.
    """
    global main_context_manager
    global main_context

    with main_context_lock:
        main_context_manager = None
        main_context = None


def get_engine():
    """Get a database engine object.
    :param use_slave: Whether to use the slave connection
    :param context: The request context that can contain a context manager
    """
    return _get_main_context_manager().writer.get_engine()


def model_query(session, model,
                args=None,
                read_deleted='no',
                project_id=None):
    """Query helper that accounts for context's `read_deleted` field.

    :param session:     The session to use, sqlalchemy.orm.session.Session
    :param model:       Model to query. Must be a subclass of ModelBase.
    :param args:        Arguments to query. If None - model is used.
    :param read_deleted: If not None, overrides context's read_deleted field.
                        Permitted values are 'no', which does not return
                        deleted values; 'only', which only returns deleted
                        values; and 'yes', which does not filter deleted
                        values.
    :param project_id:  tenant id
    """

    query_kwargs = {}
    if 'no' == read_deleted:
        query_kwargs['deleted'] = False
    elif 'only' == read_deleted:
        query_kwargs['deleted'] = True
    elif 'yes' == read_deleted:
        pass
    else:
        raise ValueError(_("Unrecognized read_deleted value '%s'")
                         % read_deleted)

    query = sqlalchemyutils.model_query(
        model, session, args, **query_kwargs)

    if project_id:
        query = query.filter_by(project_id=project_id)

    return query


@db_api.wrap_db_retry(max_retries=50, retry_interval=0.5,
                      inc_retry_interval=False, retry_on_deadlock=True)
def delete_tuple(tablename, user_id, tuple_id, project_id=None):
    with session_for_write() as session:
        try:
            query = model_query(session, tablename, project_id=project_id)
            query = query.filter_by(id=tuple_id)
            result = query.all()
            if 1 == len(result):
                result[0].delete(session=session)
                LOG.info('Tuple delete, Tuple_id: '
                         '{0} deleted in Table {1}'.
                         format(tuple_id, tablename))
            else:
                LOG.info('Tuple delete, Tuple_id: '
                         '{0} not found in Table {1}'.
                         format(tuple_id, tablename))
        except db_exc.DBError:
            message = "Database operation failed."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)
        except Exception:
            message = "An unexpected error occurred."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)
    return tuple_id


@db_api.wrap_db_retry(max_retries=50, retry_interval=0.5,
                      inc_retry_interval=False, retry_on_deadlock=True)
def get_tuple(tablename, tuple_id, project_id=None,
              all_projects=False):
    if all_projects:
        project_id = None
    with session_for_read() as session:
        try:
            query = model_query(session, tablename, project_id=project_id)
            query = query.filter_by(id=tuple_id)
            result = query.all()
        except db_exc.DBError:
            message = "Database operation failed."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)
        except Exception:
            message = "An unexpected error occurred."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)
    return result


@db_api.wrap_db_retry(max_retries=50, retry_interval=0.5,
                      inc_retry_interval=False, retry_on_deadlock=True)
def add_tuple(tuple):
    with session_for_write() as session:
        try:
            tuple.save(session=session)
        except db_exc.DBDuplicateEntry as e:
            LOG.warning('Database collision detected: {0}'.format(e))
            raise freezer_api_exc.DocumentExists(
                message='Document with the specified ID already exists.')
        except db_exc.DBError:
            message = "Database operation failed."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)
        except Exception:
            message = "An unexpected error occurred."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)


@db_api.wrap_db_retry(max_retries=50, retry_interval=0.5,
                      inc_retry_interval=False, retry_on_deadlock=True)
def update_tuple(tablename, user_id, tuple_id, tuple_values, project_id=None):

    tuple_values = dict(tuple_values)
    tuple_values['user_id'] = user_id
    tuple_values['updated_at'] = timeutils.utcnow()

    with session_for_write() as session:
        try:
            query = model_query(session, tablename, project_id=project_id)
            query = query.filter_by(id=tuple_id)
            result = query.update(tuple_values)
        except db_exc.DBDuplicateEntry as e:
            LOG.warning('Database update collision: {0}'.format(e))
            raise freezer_api_exc.DocumentExists(
                message='Update failed: document already exists.')
        except db_exc.DBError:
            message = "Database operation failed."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)
        except Exception:
            message = "An unexpected error occurred."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)

    if not result:
        raise freezer_api_exc.DocumentNotFound(
            message='Tuple not registered with ID'
                    ' {0} in Table{1} '.format(tuple_id, tablename))

    else:
        LOG.info('Tuple updated, tuple_id: {0}'.format(tuple_id))
        return tuple_id


@db_api.wrap_db_retry(max_retries=50, retry_interval=0.5,
                      inc_retry_interval=False, retry_on_deadlock=True)
def replace_tuple(tablename, user_id, tuple_id, tuple_values, project_id=None):

    tuple_values = dict(tuple_values)
    tuple_values['user_id'] = user_id
    tuple_values['updated_at'] = timeutils.utcnow()

    with session_for_write() as session:
        try:
            query = model_query(session, tablename, project_id=project_id)
            query = query.filter_by(id=tuple_id)
            result = query.update(tuple_values)
            if not result:
                tuplet = tablename()
                tuplet.update(tuple_values)
                tuplet.save(session=session)
        except db_exc.DBDuplicateEntry as e:
            LOG.warning('Database collision detected: {0}'.format(e))
            raise freezer_api_exc.DocumentExists(
                message='Document with the specified ID already exists.')
        except db_exc.DBError:
            message = "Database operation failed."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)
        except Exception:
            message = "An unexpected error occurred."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)
    return tuple_id


@db_api.wrap_db_retry(max_retries=50, retry_interval=0.5,
                      inc_retry_interval=False, retry_on_deadlock=True)
def search_tuple(tablename, project_id=None, all_projects=False,
                 offset=0, limit=100, search=None):
    search = valid_and_get_search_option(search=search)

    if all_projects:
        project_id = None

    with session_for_read() as session:
        try:
            if tablename == models.Client and not all_projects:
                # Special case for clients: include central clients even if
                # project doesn't match the requester
                query = model_query(session, tablename)
                query = query.filter(or_(
                    models.Client.project_id == project_id,
                    models.Client.is_central.is_(True)
                ))
            else:
                query = model_query(session, tablename, project_id=project_id)

            #  If search option isn't valid or set, we use limit and offset
            #  in sqlalchemy level
            if len(search) == 0:
                query = query.offset(offset)
                query = query.limit(limit)
            result = query.all()
        except db_exc.DBError:
            message = "Database operation failed."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)
        except Exception:
            message = "An unexpected error occurred."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)

    return result, search


def get_recursively(source_dict, search_keys):
    """
    Takes a dict with nested lists and dicts,
    and searches all dicts for a key of the search_keys
    provided.
    """
    search_keys_found = {}
    for key, value in source_dict.items():
        if key in search_keys.keys():
            search_keys_found[key] = value
        elif isinstance(value, dict):
            results = get_recursively(value, search_keys)
            for keytmp in results.keys():
                search_keys_found[keytmp] = results.get(keytmp)
        elif isinstance(value, list):
            for item in value:
                if isinstance(item, dict):
                    more_results = get_recursively(item, search_keys)
                    for key_another in more_results.keys():
                        search_keys_found[key_another] = \
                            more_results.get(key_another)

    return search_keys_found


def valid_and_get_search_option(search=None):
    if not search:
        return {}
    match_opt = search.get('match', {})
    match_not_opt = search.get('match_not', {})

    if not match_opt and not match_not_opt:
        return {}
    if len(match_opt) and '_all' in match_opt[0].keys():
        error_msg = '''
        Freezer-api supported by sqlalchemy:
        The option --search in freezer cmd needs a list about {key, value},
        such as:
        '[{"client_id": "node1_2"}]'
        '[{"client_id": "node1_2"}, {"status": "completed"}]'
        '''
        search = match_opt[0].get('_all')
        try:
            search = json_utils.json_decode(search)
        except Exception as e:
            msg = "{0} \n json_decode error: {1}".\
                format(error_msg, e)
            LOG.error(msg)
            return {}
        if not isinstance(search, list):
            LOG.error(error_msg)
            return {}
        search = {'match': search}
    if not isinstance(search, dict):
        return {}
    return search


def filter_tuple_by_search_opt(tuples, offset=0, limit=100, search=None):
    search = search or {}
    search_key = {}
    result_last = []
    # search opt is null, all tuples will be filtered in.
    if len(search) == 0:
        return tuples
    for m in search.get('match', []):
        for key, value in m.items():
            search_key[key] = value
    for m in search.get('match_not', []):
        for key, value in m.items():
            search_key[key] = value

    jobs_search_offset = 0
    jobs_search_count = 0
    for tuple in tuples:
        if jobs_search_count >= limit:
            return result_last
        filter_out = False
        search_keys_found = get_recursively(tuple, search_key)
        # If all keys and values are in search_keys_found, this tuple will be
        # filtered in, otherwise filtered out
        for m in search.get('match', []):
            for key, value in m.items():
                if value != search_keys_found.get(key):
                    filter_out = True
                    break
        # If one keys and values are in search_keys_found, this tuple will be
        # filtered out, otherwise filtered in.
        for m in search.get('match_not', []):
            for key, value in m.items():
                if value == search_keys_found.get(key):
                    filter_out = True
                    break
        if not filter_out:
            jobs_search_offset += 1
            if jobs_search_offset > offset:
                jobs_search_count += 1
                result_last.append(tuple)
    return result_last


@db_api.wrap_db_retry(max_retries=50, retry_interval=0.5,
                      inc_retry_interval=False, retry_on_deadlock=True)
def get_client_byid(client_id, project_id=None):

    with session_for_read() as session:
        try:
            query = model_query(session, models.Client)
            if client_id:
                # Allow retrieval if user's project owns it OR if it
                # is a central client
                query = query.filter(or_(
                    models.Client.project_id == project_id,
                    models.Client.is_central.is_(True)
                ))
                query = query.filter_by(client_id=client_id)
                result = query.all()
        except db_exc.DBError:
            message = "Database operation failed."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)
        except Exception:
            message = "An unexpected error occurred."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)

    return result


def decode_capability(db_field):
    decoded = []
    if db_field:
        decoded = json_utils.json_decode(db_field)
    return decoded


def get_client(project_id=None, client_id=None, offset=0,
               limit=100, search=None):

    clients = []
    search_key = {}
    if client_id:
        result = get_client_byid(client_id, project_id=project_id)
    else:
        result, search_key = search_tuple(tablename=models.Client,
                                          project_id=project_id, offset=offset,
                                          limit=limit, search=search)

    for client in result:
        clientmap = {}
        clientmap['project_id'] = client.project_id
        clientmap['user_id'] = client.user_id
        clientmap['client'] = {'uuid': client.uuid,
                               'hostname': client.hostname,
                               'client_id': client.client_id,
                               'is_central': client.is_central,
                               'description': client.description,
                               'supported_actions': decode_capability(
                                   client.supported_actions),
                               'supported_modes': decode_capability(
                                   client.supported_modes),
                               'supported_storages': decode_capability(
                                   client.supported_storages),
                               'supported_engines': decode_capability(
                                   client.supported_engines)}
        clients.append(clientmap)

    # If search opt is wrong, filter will not work,
    # return all tuples.
    if not client_id:
        clients = filter_tuple_by_search_opt(clients, offset=offset,
                                             limit=limit, search=search_key)
    return clients


def add_client(user_id, doc, project_id=None):

    client_doc = utilsv2.ClientDoc.create(doc, project_id, user_id)
    client_id = client_doc['client']['client_id']
    values = {}
    client_json = client_doc.get('client', {})
    is_central = doc.get('is_central', False)
    hostname = client_json.get('hostname', None)

    existing_client_uuid = None
    if is_central:
        # Central clients must be unique across all projects to prevent
        # identity hijacking. We check the identity pair (client_id, hostname)
        # globally.
        with session_for_read() as session:
            query = model_query(session, models.Client).filter_by(
                client_id=client_id, hostname=hostname)
            existing = query.first()
    else:
        result = get_client_byid(client_id, project_id=project_id)
        existing = result[0] if result else None

    if existing:
        existing_client_uuid = existing.uuid

    values = {}
    values['project_id'] = project_id
    values['client_id'] = client_id
    values['user_id'] = user_id
    values['hostname'] = hostname
    values['description'] = client_json.get('description', None)
    values['supported_actions'] = json_utils.json_encode(
        client_json.get('supported_actions', SUPPORTED_ACTIONS))
    values['supported_modes'] = json_utils.json_encode(
        client_json.get('supported_modes', SUPPORTED_MODES))
    values['supported_storages'] = json_utils.json_encode(
        client_json.get('supported_storages', SUPPORTED_STORAGES))
    values['supported_engines'] = json_utils.json_encode(
        client_json.get('supported_engines', SUPPORTED_ENGINES))
    values['is_central'] = is_central

    if existing_client_uuid:
        # Update existing client
        # We only update description and capabilities, NOT the owner or
        # identity
        update_values = {
            'description': values['description'],
            'supported_actions': values['supported_actions'],
            'supported_modes': values['supported_modes'],
            'supported_storages': values['supported_storages'],
            'supported_engines': values['supported_engines'],
        }

        # Check ownership first.
        # Otherwise it will respond with 200 while not touching the data
        if existing.project_id != project_id:
            raise freezer_api_exc.DocumentExists(
                message='Client {0} is already registered by another project'
                        .format(client_id))

        changed = any(getattr(existing, k) != v
                      for k, v in update_values.items())
        if not changed:
            LOG.info('Client registration unchanged, '
                     'client_id: {0}'.format(client_id))
            return client_id

        # update_tuple filters by project_id/user_id. If the existing client
        # belongs to another project (possible with central clients), it will
        # raise DocumentNotFound.
        try:
            update_tuple(tablename=models.Client, user_id=user_id,
                         tuple_id=existing_client_uuid,
                         tuple_values=update_values,
                         project_id=project_id)
            LOG.info('Client updated, client_id: {0}'.format(client_id))
        except freezer_api_exc.DocumentNotFound:
            # If we found it globally but couldn't update it locally,
            # it's owned by someone else
            raise freezer_api_exc.DocumentExists(
                message='Client {0} is already registered by '
                        'another project'.format(client_id))
    else:
        # Create new client
        client = models.Client()
        values['id'] = client_json.get('uuid', uuid.uuid4().hex)
        values['uuid'] = values['id']
        client.update(values)
        add_tuple(tuple=client)
        LOG.info('Client registered, client_id: {0}'.format(client_id))

    return client_id


def delete_client(user_id, client_id, project_id=None):

    existing = get_client(project_id=project_id,
                          client_id=client_id)

    if existing:
        client_info = existing[0]
        if client_info.get('project_id') != project_id:
            raise freezer_api_exc.AccessForbidden(
                "You are not permitted to delete this client"
            )

        clientt = client_info.get('client')
        clientid = clientt.get('uuid')
        delete_tuple(tablename=models.Client, user_id=user_id,
                     tuple_id=clientid, project_id=project_id)
        LOG.info('Client delete, client_id: {0} deleted'.
                 format(client_id))
    else:
        LOG.info('Client delete, client_id: {0} not found'.
                 format(client_id))
    return client_id


def delete_action(user_id, action_id, project_id=None):

    tupleid = delete_tuple(tablename=models.Action, user_id=user_id,
                           tuple_id=action_id, project_id=project_id)

    tupleid = delete_tuple(tablename=models.ActionReport, user_id=user_id,
                           tuple_id=action_id, project_id=project_id)
    return tupleid


def add_action(user_id, doc, project_id=None):

    action_doc = utilsv2.ActionDoc.create(doc, user_id, project_id)

    keyt = ['action', 'backup_name',
            'container', 'path_to_backup', 'timeout',
            'priority', 'mandatory', 'log_file']
    freezer_action = action_doc.get('freezer_action', {})

    action_id = action_doc.get('action_id')
    # Check globally to avoid primary Key collision
    existing = get_tuple(tablename=models.Action,
                         tuple_id=action_id, project_id=None,
                         all_projects=True)
    if existing:
        raise freezer_api_exc.DocumentExists(
            message='Action already registered with ID'
                    ' {0}'.format(action_id))

    action = models.Action()

    actionvalue = {}
    actionreportvalue = {}
    actionvalue['project_id'] = project_id
    actionvalue['id'] = action_id
    actionvalue['user_id'] = user_id
    actionvalue['max_retries'] = action_doc.get('max_retries', 5)
    actionvalue['max_retries_interval'] = action_doc.\
        get('max_retries_interval', 6)
    actionvalue['actionmode'] = freezer_action.get('mode', None)

    for key in freezer_action.keys():
        if key in keyt:
            actionvalue[key] = freezer_action.get(key)

    actionreportvalue['result'] = freezer_action.get('result', None)
    actionreportvalue['time_elapsed'] = freezer_action.\
        get('time_elapsed', None)
    actionreportvalue['report_date'] = freezer_action.\
        get('report_date', None)
    actionvalue['backup_metadata'] = json_utils.json_encode(freezer_action)

    action.update(actionvalue)

    add_tuple(tuple=action)

    LOG.info('Action registered, action_id: {0}'.format(action_id))

    actionReport = models.ActionReport()

    actionreportvalue['project_id'] = project_id
    actionreportvalue['id'] = action_id
    actionreportvalue['user_id'] = user_id

    actionReport.update(actionreportvalue)

    add_tuple(tuple=actionReport)
    LOG.info('Action Reports registered, action_id: {0}'.
             format(action_id))
    return action_id


def get_action(action_id, project_id=None):

    result = get_tuple(tablename=models.Action,
                       tuple_id=action_id, project_id=project_id)

    values = {}
    if 1 == len(result):
        values['project_id'] = result[0].get('project_id')
        values['action_id'] = result[0].get('id')
        values['user_id'] = result[0].get('user_id')
        values['max_retries'] = result[0].get('max_retries')
        values['max_retries_interval'] = result[0].\
            get('max_retries_interval')
        values['freezer_action'] = json_utils.\
            json_decode(result[0].get('backup_metadata'))
        values['freezer_action']['backup_name'] = result[0].\
            get('backup_name')
        values['freezer_action']['action'] = result[0].get('action')
        values['freezer_action']['mode'] = result[0].get('actionmode')
        values['freezer_action']['container'] = result[0].\
            get('container')
        values['freezer_action']['timeout'] = result[0].get('timeout')
        values['freezer_action']['priority'] = result[0].get('priority')
        values['freezer_action']['path_to_backup'] = result[0].\
            get('path_to_backup')
        values['freezer_action']['log_file'] = result[0].get('log_file')
    return values


def search_action(project_id=None, offset=0,
                  limit=100, search=None):

    actions = []

    result, search_key = search_tuple(tablename=models.Action,
                                      project_id=project_id, offset=offset,
                                      limit=limit, search=search)
    for action in result:
        actionmap = {}
        actionmap['project_id'] = project_id
        actionmap['user_id'] = action.user_id
        actionmap['timeout'] = action.timeout
        actionmap['max_retries_interval'] = action.max_retries_interval
        actionmap['max_retries'] = action.max_retries
        actionmap['action_id'] = action.id
        actionmap['mandatory'] = action.mandatory

        actionmap['freezer_action'] = json_utils.\
            json_decode(action.get('backup_metadata'))
        actionmap['freezer_action']['backup_name'] = action.\
            get('backup_name')
        actionmap['freezer_action']['mode'] = action.get('actionmode')
        actionmap['freezer_action']['action'] = action.get('action')
        actionmap['freezer_action']['container'] = action.\
            get('container')
        actionmap['freezer_action']['timeout'] = action.get('timeout')
        actionmap['freezer_action']['priority'] = action.get('priority')
        actionmap['freezer_action']['path_to_backup'] = action.\
            get('path_to_backup')
        actionmap['freezer_action']['log_file'] = action.get('log_file')

        actions.append(actionmap)
    # If search opt is wrong, filter will not work,
    # return all tuples.
    actions = filter_tuple_by_search_opt(actions, offset=offset, limit=limit,
                                         search=search_key)
    return actions


def update_action(user_id, action_id, patch_doc, project_id=None):
    # changes in user_id or action_id are not allowed
    valid_patch = utilsv2.ActionDoc.create_patch(patch_doc)

    keyt = ['action', 'backup_name', 'container',
            'path_to_backup', 'timeout', 'priority', 'mandatory', 'log_file']

    values = {}

    freezer_action = valid_patch.get('freezer_action', {})

    values['project_id'] = project_id
    values['id'] = action_id
    values['user_id'] = user_id

    values['max_retries'] = valid_patch.get('max_retries', None)
    values['max_retries_interval'] = valid_patch.\
        get('max_retries_interval', None)

    for key in freezer_action.keys():
        if key in keyt:
            values[key] = freezer_action.get(key)

    values['actionmode'] = freezer_action.get('mode', None)
    values['backup_metadata'] = json_utils.json_encode(freezer_action)

    update_tuple(tablename=models.Action, user_id=user_id, tuple_id=action_id,
                 tuple_values=values, project_id=project_id)

    return action_id


def replace_action(user_id, action_id, doc, project_id=None):

    valid_doc = utilsv2.ActionDoc.update(doc, user_id, action_id, project_id)
    values = {}
    keyt = ['action', 'backup_name', 'container',
            'path_to_backup', 'timeout', 'priority', 'mandatory', 'log_file']

    freezer_action = valid_doc.get('freezer_action', {})

    values['project_id'] = project_id
    values['id'] = action_id
    values['user_id'] = user_id

    values['max_retries'] = valid_doc.get('max_retries', None)
    values['max_retries_interval'] = valid_doc.\
        get('max_retries_interval', None)

    for key in freezer_action.keys():
        if key in keyt:
            values[key] = freezer_action.get(key)
    values['actionmode'] = freezer_action.get('mode', None)
    values['backup_metadata'] = json_utils.json_encode(freezer_action)
    replace_tuple(tablename=models.Action, user_id=user_id,
                  tuple_id=action_id, tuple_values=values,
                  project_id=project_id)

    LOG.info('action replaced, action_id: {0}'.format(action_id))
    return action_id


def check_job_client(project_id, job_actions, client_id):
    client_list = get_client(
        project_id=project_id,
        client_id=client_id)
    if not client_list:
        raise freezer_api_exc.UnprocessableEntity(
            message='Client not found with ID {0}'.format(client_id))
    client = client_list[0]
    check_client_capabilities(job_actions, client)


def delete_job(user_id, job_id, project_id=None):
    trust_id = None
    with session_for_write() as session:
        try:
            job = model_query(session, models.Job, project_id=project_id).\
                filter_by(id=job_id).first()
            if job:
                if job.user_credentials:
                    trust_id = job.user_credentials.trust_id
                    job.user_credentials.delete(session)
                job.delete(session)
        except db_exc.DBError:
            message = "Database operation failed."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)
        except Exception:
            message = "An unexpected error occurred."
            LOG.exception(message)
            raise freezer_api_exc.StorageEngineError(message=message)
    return job_id, trust_id


def add_job(user_id, doc, project_id=None):
    job_doc = utilsv2.JobDoc.create(doc, project_id, user_id)

    job_id = job_doc.get('job_id')
    # Check globally to avoid primary Key collision
    existing = get_job(project_id=None,
                       job_id=job_id, all_projects=True)
    if existing:
        raise freezer_api_exc.\
            DocumentExists(message='Job already registered with ID {0}'.
                           format(job_id))

    check_job_client(
        project_id=project_id,
        job_actions=job_doc.get('job_actions', []),
        client_id=job_doc.get('client_id'))

    job = models.Job()
    jobvalue = {}
    jobvalue['id'] = job_id
    jobvalue['project_id'] = project_id
    jobvalue['user_id'] = user_id
    jobvalue['schedule'] = json_utils.\
        json_encode(job_doc.pop('job_schedule', ''))
    jobvalue['client_id'] = job_doc.get('client_id', '')
    jobvalue['session_id'] = job_doc.pop('session_id', '')
    jobvalue['session_tag'] = job_doc.pop('session_tag', 0)
    jobvalue['description'] = job_doc.pop('description', '')
    jobvalue['job_actions'] = json_utils.\
        json_encode(job_doc.pop('job_actions', ''))
    job.update(jobvalue)

    if 'user_credentials' in job_doc:
        job.user_credentials = models.UserCredentials(
            id=uuid.uuid4().hex,
            trust_id=job_doc['user_credentials']['trust_id'],
            trustor_user_id=job_doc['user_credentials']['trustor_user_id'],
            job_id=job_id)

    add_tuple(tuple=job)

    LOG.info('Job registered, job_id: {0}'.format(job_id))

    return job_id


def get_job(job_id, project_id=None, all_projects=False):

    result = get_tuple(tablename=models.Job,
                       tuple_id=job_id, project_id=project_id,
                       all_projects=all_projects)
    values = {}
    if 1 == len(result):
        values['job_id'] = result[0].get('id')
        values['project_id'] = result[0].get('project_id')
        values['user_id'] = result[0].get('user_id')
        values['job_schedule'] = json_utils.\
            json_decode(result[0].get('schedule'))
        values['client_id'] = result[0].get('client_id')
        values['session_id'] = result[0].get('session_id')
        values['session_tag'] = result[0].get('session_tag')
        values['description'] = result[0].get('description')
        values['job_actions'] = json_utils.\
            json_decode(result[0].get('job_actions'))
        user_credentials = result[0].get('user_credentials', None)
        if user_credentials:
            values['user_credentials'] = {
                'trust_id': user_credentials.get('trust_id'),
                'trustor_user_id': user_credentials.get('trustor_user_id'),
            }
    return values


def trust_in_use(trust_id):
    with session_for_read() as session:
        query = model_query(session, models.UserCredentials)
        query = query.filter_by(trust_id=trust_id)
        return query.count() > 0


def search_job(project_id=None, all_projects=False, offset=0,
               limit=100, search=None):
    jobs = []
    result, search_key = search_tuple(tablename=models.Job,
                                      project_id=project_id,
                                      all_projects=all_projects,
                                      offset=offset, limit=limit,
                                      search=search)
    for job in result:
        jobmap = {}
        jobmap['job_id'] = job.get('id')
        jobmap['project_id'] = job.get('project_id')
        jobmap['user_id'] = job.get('user_id')
        jobmap['job_schedule'] = json_utils.json_decode(job.get('schedule'))
        jobmap['client_id'] = job.get('client_id')
        jobmap['session_id'] = job.get('session_id')
        jobmap['session_tag'] = job.get('session_tag')
        jobmap['description'] = job.get('description')
        jobmap['job_actions'] = json_utils.json_decode(
            job.get('job_actions'))
        user_credentials = job.get('user_credentials', None)
        if user_credentials:
            jobmap['user_credentials'] = {
                'trust_id': user_credentials.get('trust_id'),
                'trustor_user_id': user_credentials.get('trustor_user_id'),
            }

        jobs.append(jobmap)
    # If search opt is wrong, filter will not work,
    # return all tuples.
    jobs = filter_tuple_by_search_opt(jobs, offset=offset, limit=limit,
                                      search=search_key)
    return jobs


def update_job(user_id, job_id, patch_doc, project_id=None):

    valid_patch = utilsv2.JobDoc.create_patch(patch_doc)

    if 'job_actions' in valid_patch:
        client_id = valid_patch.get(
            'client_id',
            get_job(project_id=project_id, job_id=job_id
                    ).get('client_id')
        )
        check_job_client(
            project_id=project_id,
            job_actions=valid_patch.get('job_actions', []),
            client_id=client_id)

    values = {}
    for key in valid_patch.keys():
        if key == 'job_schedule':
            values['schedule'] = json_utils.\
                json_encode(valid_patch.get(key, None))
        elif key == 'job_actions':
            values[key] = json_utils.json_encode(valid_patch.get(key, None))
        else:
            values[key] = valid_patch.get(key, None)

    update_tuple(tablename=models.Job, user_id=user_id, tuple_id=job_id,
                 tuple_values=values, project_id=project_id)

    return 0


def replace_job(user_id, job_id, doc, project_id=None):

    valid_doc = utilsv2.JobDoc.update(doc, user_id, job_id, project_id)

    check_job_client(
        project_id=project_id,
        job_actions=valid_doc.get('job_actions', []),
        client_id=valid_doc.get('client_id'))

    values = {}
    valuesnew = {}

    values['id'] = job_id
    values['project_id'] = project_id
    values['user_id'] = user_id
    values['schedule'] = json_utils.\
        json_encode(valid_doc.pop('job_schedule', ''))
    values['client_id'] = valid_doc.get('client_id', '')
    values['session_id'] = valid_doc.pop('session_id', '')
    values['session_tag'] = valid_doc.pop('session_tag', 0)
    values['description'] = valid_doc.pop('description', '')
    values['job_actions'] = json_utils.\
        json_encode(valid_doc.pop('job_actions', ''))

    for key in values:
        if values[key] is not None:
            valuesnew[key] = values[key]

    replace_tuple(tablename=models.Job, user_id=user_id,
                  tuple_id=job_id, tuple_values=valuesnew,
                  project_id=project_id)
    LOG.info('job replaced, job_id: {0}'.format(job_id))
    return job_id


def get_backup(backup_id, project_id=None):
    result = get_tuple(tablename=models.Backup,
                       tuple_id=backup_id, project_id=project_id)
    values = {}
    if 1 == len(result):
        values['project_id'] = result[0].get('project_id')
        values['backup_id'] = result[0].get('id')
        values['user_id'] = result[0].get('user_id')
        values['backup_metadata'] = json_utils.\
            json_decode(result[0].get('backup_metadata'))
    return values


def add_backup(user_id, doc, project_id=None):

    metadatadoc = utilsv2.BackupMetadataDoc(project_id, user_id, '',
                                            doc)
    if not metadatadoc.is_valid():
        raise freezer_api_exc.BadDataFormat(
            message='Bad Data Format')
    backup_id = metadatadoc.backup_id
    backupjson = metadatadoc.serialize()
    backup_metadata = backupjson.get('backup_metadata')

    existing = get_backup(project_id=project_id,
                          backup_id=backup_id)
    if existing:
        raise freezer_api_exc.DocumentExists(
            message='Backup already registered with ID'
                    ' {0}'.format(backup_id))

    backup = models.Backup()
    backupvalue = {}
    backupvalue['project_id'] = project_id
    backupvalue['id'] = backup_id
    backupvalue['user_id'] = user_id
    backupvalue['job_id'] = backup_metadata.get('job_id')
    # The field backup_metadata is json, including :
    # hostname , backup_name , container etc
    backupvalue['backup_metadata'] = json_utils.json_encode(backup_metadata)
    backup.update(backupvalue)

    add_tuple(tuple=backup)
    LOG.info('Backup registered, backup_id: {0}'.format(backup_id))
    return backup_id


def delete_backup(user_id, backup_id, project_id=None):

    tupleid = delete_tuple(tablename=models.Backup, user_id=user_id,
                           tuple_id=backup_id, project_id=project_id)
    return tupleid


def search_backup(project_id=None, offset=0,
                  limit=100, search=None):
    backups = []

    result, search_key = search_tuple(tablename=models.Backup,
                                      project_id=project_id, offset=offset,
                                      limit=limit, search=search)
    for backup in result:
        backupmap = {}
        backupmap['project_id'] = project_id
        backupmap['user_id'] = backup.user_id
        backupmap['backup_id'] = backup.id
        backupmap['backup_metadata'] = json_utils.\
            json_decode(backup.get('backup_metadata'))
        backups.append(backupmap)
    # If search opt is wrong, filter will not work,
    # return all tuples.
    backups = filter_tuple_by_search_opt(backups, offset=offset, limit=limit,
                                         search=search_key)
    return backups


def get_session(session_id, project_id=None):
    jobt = {}
    values = {}
    result = get_tuple(tablename=models.Session,
                       tuple_id=session_id, project_id=project_id)
    if 1 == len(result):
        values['project_id'] = result[0].get('project_id')
        values['session_id'] = result[0].get('id')
        values['user_id'] = result[0].get('user_id')
        values['description'] = result[0].get('description')
        values['session_tag'] = result[0].get('session_tag')
        values['result'] = result[0].get('result')
        values['hold_off'] = result[0].get('hold_off')
        values['status'] = result[0].get('status')
        values['time_ended'] = result[0].get('time_ended')
        values['time_started'] = result[0].get('time_started')
        values['time_end'] = result[0].get('time_end')
        values['time_start'] = result[0].get('time_start')

        values['schedule'] = json_utils.json_decode(result[0].
                                                    get('schedule'))
        jobt = result[0].get('job')
        if jobt is not None:
            values['jobs'] = json_utils.json_decode(result[0].get('job'))

    return values


def delete_session(user_id, session_id, project_id=None):

    tupleid = delete_tuple(tablename=models.Session, user_id=user_id,
                           tuple_id=session_id, project_id=project_id)
    return tupleid


def add_session(user_id, doc, project_id=None):

    session_doc = utilsv2.SessionDoc.create(doc=doc,
                                            user_id=user_id,
                                            project_id=project_id)

    session_id = session_doc['session_id']
    schedulingjson = session_doc.get('schedule')
    existing = get_session(project_id=project_id,
                           session_id=session_id)
    if existing:
        raise freezer_api_exc.DocumentExists(
            message='Session already registered with ID'
                    ' {0}'.format(session_id))

    sessiont = models.Session()
    sessionvalue = {}
    sessionvalue['project_id'] = project_id
    sessionvalue['id'] = session_id
    sessionvalue['user_id'] = user_id
    sessionvalue['description'] = session_doc.get('description', None)
    sessionvalue['hold_off'] = session_doc.get('hold_off', 30)
    sessionvalue['session_tag'] = session_doc.get('session_tag', 0)
    sessionvalue['status'] = session_doc.get('status')
    sessionvalue['time_ended'] = session_doc.get('time_ended', -1)
    sessionvalue['time_started'] = session_doc.get('time_started', -1)
    sessionvalue['time_end'] = session_doc.get('time_end', -1)
    sessionvalue['time_start'] = session_doc.get('time_start', -1)
    sessionvalue['result'] = session_doc.get('result', None)

    # The field scheduling is json
    sessionvalue['schedule'] = json_utils.json_encode(schedulingjson)
    sessiont.update(sessionvalue)

    add_tuple(tuple=sessiont)
    LOG.info('Session registered, session_id: {0}'.format(session_id))

    return session_id


def update_session(user_id, session_id, patch_doc, project_id=None):

    valid_patch = utilsv2.SessionDoc.create_patch(patch_doc)

    sessiont = get_session(project_id=project_id,
                           session_id=session_id)
    if not sessiont:
        raise freezer_api_exc.DocumentNotFound(
            message='Session not register with ID'
                    ' {0}'.format(session_id))

    values = {}
    for key in valid_patch.keys():
        if key == 'jobs':
            jobintable = sessiont.get('jobs', None)
            jobinpatch = valid_patch.get('jobs', None)
            if jobintable:
                jobintable.update(jobinpatch)
                jobinpatch.update(jobintable)
            values['job'] = json_utils.json_encode(valid_patch.get(key, None))
        elif key == 'schedule':
            values[key] = json_utils.json_encode(valid_patch.get(key, None))
        else:
            values[key] = valid_patch.get(key, None)

    update_tuple(tablename=models.Session, user_id=user_id,
                 tuple_id=session_id, tuple_values=values,
                 project_id=project_id)

    return 0


def replace_session(user_id, session_id, doc, project_id=None):

    valid_doc = utilsv2.SessionDoc.update(doc, user_id, session_id,
                                          project_id)
    values = {}
    valuesnew = {}

    values['id'] = session_id
    values['project_id'] = project_id
    values['user_id'] = user_id
    values['schedule'] = json_utils.\
        json_encode(valid_doc.get('schedule', None))
    values['job'] = json_utils.json_encode(valid_doc.get('jobs', None))
    values['session_tag'] = valid_doc.get('session_tag', None)
    values['description'] = valid_doc.get('description', None)
    values['status'] = valid_doc.get('status')
    values['time_ended'] = valid_doc.get('time_ended', None)
    values['time_started'] = valid_doc.get('time_started', None)
    values['time_end'] = valid_doc.get('time_end', None)
    values['time_start'] = valid_doc.get('time_start', None)
    values['result'] = valid_doc.get('result', None)
    values['hold_off'] = valid_doc.get('hold_off', None)

    for key in values:
        if values[key] is not None:
            valuesnew[key] = values[key]

    replace_tuple(tablename=models.Session, user_id=user_id,
                  tuple_id=session_id,
                  tuple_values=valuesnew,
                  project_id=project_id)
    LOG.info('session replaced, session_id: {0}'.format(session_id))
    return session_id


def search_session(project_id=None, offset=0,
                   limit=100, search=None):
    sessions = []
    jobt = {}

    result, search_key = search_tuple(tablename=models.Session,
                                      project_id=project_id, offset=offset,
                                      limit=limit, search=search)
    for sessiont in result:
        sessionmap = {}
        sessionmap['project_id'] = project_id
        sessionmap['user_id'] = sessiont.get('user_id')
        sessionmap['session_id'] = sessiont.get('id')
        sessionmap['description'] = sessiont.get('description')
        sessionmap['session_tag'] = sessiont.get('session_tag')
        sessionmap['result'] = sessiont.get('result')
        sessionmap['hold_off'] = sessiont.get('hold_off')
        sessionmap['status'] = sessiont.get('status')
        sessionmap['time_ended'] = sessiont.get('time_ended')
        sessionmap['time_end'] = sessiont.get('time_end')
        sessionmap['time_started'] = sessiont.get('time_started')
        sessionmap['time_start'] = sessiont.get('time_start')
        sessionmap['schedule'] = json_utils.\
            json_decode(sessiont.get('schedule'))
        jobt = sessiont.get('job')
        if jobt is not None:
            sessionmap['jobs'] = json_utils.json_decode(sessiont.get('job'))
        sessions.append(sessionmap)
    # If search opt is wrong, filter will not work,
    # return all tuples.
    sessions = filter_tuple_by_search_opt(sessions, offset=offset,
                                          limit=limit, search=search_key)

    return sessions
