# -------------------------------------------------------------------------
#
#  Part of the CodeChecker project, under the Apache License v2.0 with
#  LLVM Exceptions. See LICENSE for license information.
#  SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
# -------------------------------------------------------------------------
"""
Handle Thrift requests.
"""


import base64
import os
import re
import shlex
import tempfile
import time
import zipfile
import zlib

from collections import defaultdict
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Set

import sqlalchemy
from sqlalchemy.sql.expression import or_, and_, not_, func, \
    asc, desc, union_all, select, bindparam, literal_column, cast

import codechecker_api_shared
from codechecker_api.codeCheckerDBAccess_v6 import constants, ttypes
from codechecker_api.codeCheckerDBAccess_v6.ttypes import BugPathPos, \
    CheckerCount, CommentData, DiffType, Encoding, RunHistoryData, Order, \
    ReportData, ReportDetails, ReviewData, RunData, RunFilter, \
    RunReportCount, RunSortType, RunTagCount, SourceComponentData, \
    SourceFileData, SortMode, SortType, ExportData

from codechecker_common import plist_parser, skiplist_handler
from codechecker_common.source_code_comment_handler import \
    SourceCodeCommentHandler, SpellException, contains_codechecker_comment
from codechecker_common import util
from codechecker_common.logger import get_logger
from codechecker_report_hash.hash import get_report_path_hash

from codechecker_web.shared import webserver_context
from codechecker_web.shared import convert

from codechecker_server.profiler import timeit

from .. import permissions
from ..database import db_cleanup
from ..database.config_db_model import Product
from ..database.database import conv, DBSession, escape_like
from ..database.run_db_model import \
    AnalyzerStatistic, Report, ReviewStatus, File, Run, RunHistory, \
    RunLock, Comment, BugPathEvent, BugReportPoint, \
    FileContent, SourceComponent, ExtendedReportData
from ..metadata import checker_is_unavailable, get_analyzer_name, \
    MetadataInfoParser
from ..tmp import TemporaryDirectory

from .thrift_enum_helper import detection_status_enum, \
    detection_status_str, review_status_enum, review_status_str, \
    report_extended_data_type_enum

from . import store_handler

LOG = get_logger('server')

GEN_OTHER_COMPONENT_NAME = "Other (auto-generated)"


class CommentKindValue(object):
    USER = 0
    SYSTEM = 1


def comment_kind_from_thrift_type(kind):
    """ Convert the given comment kind from Thrift type to Python enum. """
    if kind == ttypes.CommentKind.USER:
        return CommentKindValue.USER
    elif kind == ttypes.CommentKind.SYSTEM:
        return CommentKindValue.SYSTEM


def comment_kind_to_thrift_type(kind):
    """ Convert the given comment kind from Python enum to Thrift type. """
    if kind == CommentKindValue.USER:
        return ttypes.CommentKind.USER
    elif kind == CommentKindValue.SYSTEM:
        return ttypes.CommentKind.SYSTEM


def verify_limit_range(limit):
    """Verify limit value for the queries.

    Query limit should not be larger than the max allowed value.
    Max is returned if the value is larger than max.
    """
    max_query_limit = constants.MAX_QUERY_SIZE
    if not limit:
        return max_query_limit
    if limit > max_query_limit:
        LOG.warning('Query limit %d was larger than max query limit %d, '
                    'setting limit to %d',
                    limit,
                    max_query_limit,
                    max_query_limit)
        limit = max_query_limit
    return limit


def slugify(text):
    """
    Removes and replaces special characters in a given text.
    """
    # Removes non-alpha characters.
    norm_text = re.sub(r'[^\w\s\-/]', '', text)

    # Converts spaces and slashes to underscores.
    norm_text = re.sub(r'([\s]+|[/]+)', '_', norm_text)

    return norm_text


def exc_to_thrift_reqfail(func):
    """
    Convert internal exceptions to RequestFailed exception
    which can be sent back on the thrift connections.
    """
    func_name = func.__name__

    def wrapper(*args, **kwargs):
        try:
            res = func(*args, **kwargs)
            return res

        except sqlalchemy.exc.SQLAlchemyError as alchemy_ex:
            # Convert SQLAlchemy exceptions.
            msg = str(alchemy_ex)
            raise codechecker_api_shared.ttypes.RequestFailed(
                codechecker_api_shared.ttypes.ErrorCode.DATABASE, msg)
        except codechecker_api_shared.ttypes.RequestFailed as rf:
            LOG.warning("%s:\n%s", func_name, rf.message)
            raise
        except Exception as ex:
            msg = str(ex)
            LOG.warning("%s:\n%s", func_name, msg)
            raise codechecker_api_shared.ttypes.RequestFailed(
                codechecker_api_shared.ttypes.ErrorCode.GENERAL, msg)

    return wrapper


def parse_codechecker_review_comment(source_file_name,
                                     report_line,
                                     checker_name):
    """Parse the CodeChecker review comments from a source file at a given
    position.  Returns an empty list if there are no comments.
    """
    src_comment_data = []
    with open(source_file_name,
              encoding='utf-8',
              errors='ignore') as sf:
        if contains_codechecker_comment(sf):
            sc_handler = SourceCodeCommentHandler()
            try:
                src_comment_data = sc_handler.filter_source_line_comments(
                    sf,
                    report_line,
                    checker_name)
            except SpellException as ex:
                LOG.warning("File %s contains %s", source_file_name, ex)
    return src_comment_data


def get_component_values(session, component_name):
    """
    Get component values by component names and returns a tuple where the
    first item contains a list path which should be skipped and the second
    item contains a list of path which should be included.
    E.g.:
      +/a/b/x.cpp
      +/a/b/y.cpp
      -/a/b
    On the above component value this function will return the following:
      (['/a/b'], ['/a/b/x.cpp', '/a/b/y.cpp'])
    """
    components = session.query(SourceComponent) \
        .filter(SourceComponent.name.like(component_name)) \
        .all()

    skip = []
    include = []

    for component in components:
        values = component.value.decode('utf-8').split('\n')
        for value in values:
            value = value.strip()
            if not value:
                continue

            v = value[1:]
            if value[0] == '+':
                include.append(v)
            elif value[0] == '-':
                skip.append(v)

    return skip, include


def process_report_filter(session, run_ids, report_filter, cmp_data=None):
    """
    Process the new report filter.
    """
    AND = []

    cmp_filter_expr = process_cmp_data_filter(session, run_ids, report_filter,
                                              cmp_data)
    if cmp_filter_expr is not None:
        AND.append(cmp_filter_expr)

    if report_filter is None:
        return and_(*AND)

    if report_filter.filepath:
        OR = [File.filepath.ilike(conv(fp))
              for fp in report_filter.filepath]

        AND.append(or_(*OR))

    if report_filter.checkerMsg:
        OR = [Report.checker_message.ilike(conv(cm))
              for cm in report_filter.checkerMsg]
        AND.append(or_(*OR))

    if report_filter.checkerName:
        OR = [Report.checker_id.ilike(conv(cn))
              for cn in report_filter.checkerName]
        AND.append(or_(*OR))

    if report_filter.analyzerNames:
        OR = [Report.analyzer_name.ilike(conv(an))
              for an in report_filter.analyzerNames]
        AND.append(or_(*OR))

    if report_filter.runName:
        OR = [Run.name.ilike(conv(rn))
              for rn in report_filter.runName]
        AND.append(or_(*OR))

    if report_filter.reportHash:
        OR = []
        no_joker = []

        for rh in report_filter.reportHash:
            if '*' in rh:
                OR.append(Report.bug_id.ilike(conv(rh)))
            else:
                no_joker.append(rh)

        if no_joker:
            OR.append(Report.bug_id.in_(no_joker))

        AND.append(or_(*OR))

    if report_filter.severity:
        AND.append(Report.severity.in_(report_filter.severity))

    if report_filter.detectionStatus:
        dst = list(map(detection_status_str,
                       report_filter.detectionStatus))
        AND.append(Report.detection_status.in_(dst))

    if report_filter.reviewStatus:
        OR = [ReviewStatus.status.in_(
            list(map(review_status_str, report_filter.reviewStatus)))]

        # No database entry for unreviewed reports
        if (ttypes.ReviewStatus.UNREVIEWED in
                report_filter.reviewStatus):
            OR.append(ReviewStatus.status.is_(None))

        AND.append(or_(*OR))

    if report_filter.firstDetectionDate is not None:
        date = datetime.fromtimestamp(report_filter.firstDetectionDate)
        AND.append(Report.detected_at >= date)

    if report_filter.fixDate is not None:
        date = datetime.fromtimestamp(report_filter.fixDate)
        AND.append(Report.detected_at < date)

    if report_filter.date:
        detected_at = report_filter.date.detected
        if detected_at:
            if detected_at.before:
                detected_before = datetime.fromtimestamp(detected_at.before)
                AND.append(Report.detected_at <= detected_before)

            if detected_at.after:
                detected_after = datetime.fromtimestamp(detected_at.after)
                AND.append(Report.detected_at >= detected_after)

        fixed_at = report_filter.date.fixed
        if fixed_at:
            if fixed_at.before:
                fixed_before = datetime.fromtimestamp(fixed_at.before)
                AND.append(Report.fixed_at <= fixed_before)

            if fixed_at.after:
                fixed_after = datetime.fromtimestamp(fixed_at.after)
                AND.append(Report.fixed_at >= fixed_after)

    if report_filter.runHistoryTag:
        OR = []
        for history_date in report_filter.runHistoryTag:
            date = datetime.strptime(history_date,
                                     '%Y-%m-%d %H:%M:%S.%f')
            OR.append(and_(Report.detected_at <= date, or_(
                Report.fixed_at.is_(None), Report.fixed_at >= date)))
        AND.append(or_(*OR))

    if report_filter.componentNames:
        AND.append(process_source_component_filter(
            session, report_filter.componentNames))

    if report_filter.bugPathLength is not None:
        min_path_length = report_filter.bugPathLength.min
        if min_path_length is not None:
            AND.append(Report.path_length >= min_path_length)

        max_path_length = report_filter.bugPathLength.max
        if max_path_length is not None:
            AND.append(Report.path_length <= max_path_length)

    filter_expr = and_(*AND)
    return filter_expr


def process_source_component_filter(session, component_names):
    """ Process source component filter.

    The virtual auto-generated Other component will be handled separately and
    the query part will be added to the filter.
    """
    OR = []

    for component_name in component_names:
        if component_name == GEN_OTHER_COMPONENT_NAME:
            file_query = get_other_source_component_file_query(session)
        else:
            file_query = get_source_component_file_query(session,
                                                         component_name)

        if file_query is not None:
            OR.append(file_query)

    return or_(*OR)


def filter_open_reports_in_tags(results, run_ids, tag_ids):
    """
    Adding filters on "results" query which filter on open reports in
    given runs and tags.
    """

    if run_ids:
        results = results.filter(Report.run_id.in_(run_ids))

    if tag_ids:
        results = results.outerjoin(
            RunHistory, RunHistory.run_id == Report.run_id) \
            .filter(RunHistory.id.in_(tag_ids)) \
            .filter(get_open_reports_date_filter_query())

    return results


def get_source_component_file_query(session, component_name):
    """ Get filter query for a single source component. """
    skip, include = get_component_values(session, component_name)

    if skip and include:
        include_q = select([File.id]) \
            .where(or_(*[
                File.filepath.like(conv(fp)) for fp in include])) \
            .distinct()

        skip_q = select([File.id]) \
            .where(or_(*[
                File.filepath.like(conv(fp)) for fp in skip])) \
            .distinct()

        return File.id.in_(include_q.except_(skip_q))
    elif include:
        return or_(*[File.filepath.like(conv(fp)) for fp in include])
    elif skip:
        return and_(*[not_(File.filepath.like(conv(fp))) for fp in skip])


def get_other_source_component_file_query(session):
    """ Get filter query for the auto-generated Others component.
    If there are no user defined source components in the database this
    function will return with None.
    """
    component_names = session.query(SourceComponent.name).all()

    # If there are no user defined source components we don't have to filter.
    if not component_names:
        return None

    file_queries = [get_source_component_file_query(session, component_name)
                    for (component_name, ) in component_names]

    q = select([File.id]) \
        .where(or_(*file_queries)) \
        .distinct()

    return File.id.notin_(q)


def get_open_reports_date_filter_query(tbl=Report, date=RunHistory.time):
    """ Get open reports date filter. """
    return and_(tbl.detected_at <= date,
                or_(tbl.fixed_at.is_(None),
                    tbl.fixed_at > date))


def get_diff_bug_id_query(session, run_ids, tag_ids, open_reports_date):
    """ Get bug id query for diff. """
    q = session.query(Report.bug_id.distinct())
    if run_ids:
        q = q.filter(Report.run_id.in_(run_ids))

    if tag_ids:
        q = q.outerjoin(RunHistory,
                        RunHistory.run_id == Report.run_id) \
            .filter(RunHistory.id.in_(tag_ids)) \
            .filter(get_open_reports_date_filter_query())

    if open_reports_date:
        date = datetime.fromtimestamp(open_reports_date)

        q = q.filter(get_open_reports_date_filter_query(Report, date))

    return q


def get_diff_run_id_query(session, run_ids, tag_ids):
    """ Get run id query for diff. """
    q = session.query(Run.id.distinct())

    if run_ids:
        q = q.filter(Run.id.in_(run_ids))

    if tag_ids:
        q = q.outerjoin(RunHistory,
                        RunHistory.run_id == Run.id) \
            .filter(RunHistory.id.in_(tag_ids))

    return q


def is_cmp_data_empty(cmp_data):
    """ True if the parameter is None or no filter fields are set. """
    if not cmp_data:
        return True

    return not any([cmp_data.runIds,
                    cmp_data.runTag,
                    cmp_data.openReportsDate])


def is_baseline_empty(report_filter):
    """ True if the parameter is None or no baseline filter fields are set. """
    if not report_filter:
        return True

    return not any([report_filter.runTag,
                    report_filter.openReportsDate])


def process_cmp_data_filter(session, run_ids, report_filter, cmp_data):
    """ Process compare data filter. """
    base_tag_ids = report_filter.runTag if report_filter else None
    base_open_reports_date = report_filter.openReportsDate \
        if report_filter else None
    query_base = get_diff_bug_id_query(session, run_ids, base_tag_ids,
                                       base_open_reports_date)
    query_base_runs = get_diff_run_id_query(session, run_ids, base_tag_ids)

    if is_cmp_data_empty(cmp_data):
        if not run_ids and is_baseline_empty(report_filter):
            return None

        return and_(Report.bug_id.in_(query_base),
                    Report.run_id.in_(query_base_runs))

    query_new = get_diff_bug_id_query(session, cmp_data.runIds,
                                      cmp_data.runTag,
                                      cmp_data.openReportsDate)
    query_new_runs = get_diff_run_id_query(session, cmp_data.runIds,
                                           cmp_data.runTag)

    AND = []
    if cmp_data.diffType == DiffType.NEW:
        return and_(Report.bug_id.in_(query_new.except_(query_base)),
                    Report.run_id.in_(query_new_runs))

    elif cmp_data.diffType == DiffType.RESOLVED:
        return and_(Report.bug_id.in_(query_base.except_(query_new)),
                    Report.run_id.in_(query_base_runs))

    elif cmp_data.diffType == DiffType.UNRESOLVED:
        return and_(Report.bug_id.in_(query_base.intersect(query_new)),
                    Report.run_id.in_(query_new_runs))

    else:
        raise codechecker_api_shared.ttypes.RequestFailed(
            codechecker_api_shared.ttypes.ErrorCode.DATABASE,
            'Unsupported diff type: ' + str(cmp_data.diffType))

    return and_(*AND)


def process_run_history_filter(query, run_ids, run_history_filter):
    """
    Process run history filter.
    """
    if run_ids:
        query = query.filter(RunHistory.run_id.in_(run_ids))

    if run_history_filter:
        if run_history_filter.tagNames:
            OR = [RunHistory.version_tag.ilike('{0}'.format(conv(
                escape_like(name, '\\'))), escape='\\') for
                name in run_history_filter.tagNames]

            query = query.filter(or_(*OR))

        if run_history_filter.tagIds:
            query = query.filter(RunHistory.id.in_(run_history_filter.tagIds))

        stored = run_history_filter.stored
        if stored:
            if stored.before:
                stored_before = datetime.fromtimestamp(stored.before)
                query = query.filter(RunHistory.time <= stored_before)

            if stored.after:
                stored_after = datetime.fromtimestamp(stored.after)
                query = query.filter(RunHistory.time >= stored_after)

    return query


def process_run_filter(session, query, run_filter):
    """
    Process run filter.
    """
    if run_filter is None:
        return query

    if run_filter.ids:
        query = query.filter(Run.id.in_(run_filter.ids))
    if run_filter.names:
        if run_filter.exactMatch:
            query = query.filter(Run.name.in_(run_filter.names))
        else:
            OR = [Run.name.ilike('{0}'.format(conv(
                escape_like(name, '\\'))), escape='\\') for
                name in run_filter.names]
            query = query.filter(or_(*OR))

    if run_filter.beforeTime:
        date = datetime.fromtimestamp(run_filter.beforeTime)
        query = query.filter(Run.date < date)

    if run_filter.afterTime:
        date = datetime.fromtimestamp(run_filter.afterTime)
        query = query.filter(Run.date > date)

    if run_filter.beforeRun:
        run = session.query(Run.date) \
            .filter(Run.name == run_filter.beforeRun) \
            .one_or_none()

        if run:
            query = query.filter(Run.date < run.date)

    if run_filter.afterRun:
        run = session.query(Run.date) \
            .filter(Run.name == run_filter.afterRun) \
            .one_or_none()

        if run:
            query = query.filter(Run.date > run.date)

    return query


def get_report_details(session, report_ids):
    """
    Returns report details for the given report ids.
    """
    details = {}

    # Get bug path events.
    bug_path_events = session.query(BugPathEvent, File.filepath) \
        .filter(BugPathEvent.report_id.in_(report_ids)) \
        .outerjoin(File,
                   File.id == BugPathEvent.file_id) \
        .order_by(BugPathEvent.report_id, BugPathEvent.order)

    bug_events_list = defaultdict(list)
    for event, file_path in bug_path_events:
        report_id = event.report_id
        event = bugpathevent_db_to_api(event)
        event.filePath = file_path
        bug_events_list[report_id].append(event)

    # Get bug report points.
    bug_report_points = session.query(BugReportPoint, File.filepath) \
        .filter(BugReportPoint.report_id.in_(report_ids)) \
        .outerjoin(File,
                   File.id == BugReportPoint.file_id) \
        .order_by(BugReportPoint.report_id, BugReportPoint.order)

    bug_point_list = defaultdict(list)
    for bug_point, file_path in bug_report_points:
        report_id = bug_point.report_id
        bug_point = bugreportpoint_db_to_api(bug_point)
        bug_point.filePath = file_path
        bug_point_list[report_id].append(bug_point)

    # Get extended report data.
    extended_data_list = defaultdict(list)
    q = session.query(ExtendedReportData, File.filepath) \
        .filter(ExtendedReportData.report_id.in_(report_ids)) \
        .outerjoin(File,
                   File.id == ExtendedReportData.file_id)

    for data, file_path in q:
        report_id = data.report_id
        extended_data = extended_data_db_to_api(data)
        extended_data.filePath = file_path
        extended_data_list[report_id].append(extended_data)

    # Get Comments for report data
    comment_data_list = defaultdict(list)
    comment_query = session.query(Comment, Report.id)\
        .filter(Report.id.in_(report_ids)) \
        .outerjoin(Report, Report.bug_id == Comment.bug_hash) \
        .order_by(Comment.created_at.desc())

    for data, report_id in comment_query:
        report_id = report_id
        comment_data = comment_data_db_to_api(data)
        comment_data_list[report_id].append(comment_data)

    for report_id in report_ids:
        details[report_id] = \
            ReportDetails(pathEvents=bug_events_list[report_id],
                          executionPath=bug_point_list[report_id],
                          extendedData=extended_data_list[report_id],
                          comments=comment_data_list[report_id])

    return details


def bugpathevent_db_to_api(bpe):
    return ttypes.BugPathEvent(
        startLine=bpe.line_begin,
        startCol=bpe.col_begin,
        endLine=bpe.line_end,
        endCol=bpe.col_end,
        msg=bpe.msg,
        fileId=bpe.file_id)


def bugreportpoint_db_to_api(brp):
    return BugPathPos(
        startLine=brp.line_begin,
        startCol=brp.col_begin,
        endLine=brp.line_end,
        endCol=brp.col_end,
        fileId=brp.file_id)


def extended_data_db_to_api(erd):
    return ttypes.ExtendedReportData(
        type=report_extended_data_type_enum(erd.type),
        startLine=erd.line_begin,
        startCol=erd.col_begin,
        endLine=erd.line_end,
        endCol=erd.col_end,
        message=erd.message,
        fileId=erd.file_id)


def comment_data_db_to_api(comm):
    """
    Returns a CommentData Object with all the relevant fields
    """
    return ttypes.CommentData(
        id=comm.id,
        author=comm.author,
        message=get_comment_msg(comm),
        createdAt=str(comm.created_at),
        kind=comm.kind
    )


def get_comment_msg(comment):
    """
    Checks for the comment kind. If the comment is
    identified as a system comment, it is formatted accordindly.
    """
    context = webserver_context.get_context()
    message = comment.message.decode('utf-8')
    sys_comment = comment_kind_from_thrift_type(ttypes.CommentKind.SYSTEM)

    if comment.kind == sys_comment:
        try:
            elements = shlex.split(message)
        except ValueError:
            # In earlier CodeChecker we saved system comments
            # without escaping special characters such as
            # quotes. This is kept only for backward
            # compatibility reason.
            message = message \
                .replace("'", "\\'") \
                .replace('"', '\\"')

            elements = shlex.split(message)

        system_comment = context.system_comment_map.get(elements[0])
        if system_comment:
            for idx, value in enumerate(elements[1:]):
                system_comment = system_comment.replace(
                    '{' + str(idx) + '}', value)
            message = system_comment

    return message


def unzip(b64zip, output_dir):
    """
    This function unzips the base64 encoded zip file. This zip is extracted
    to a temporary directory and the ZIP is then deleted. The function returns
    the size of the extracted decompressed zip file.
    """
    if len(b64zip) == 0:
        return 0

    with tempfile.NamedTemporaryFile(suffix='.zip') as zip_file:
        LOG.debug("Unzipping mass storage ZIP '%s' to '%s'...",
                  zip_file.name, output_dir)

        zip_file.write(zlib.decompress(base64.b64decode(b64zip)))
        with zipfile.ZipFile(zip_file, 'r', allowZip64=True) as zipf:
            try:
                zipf.extractall(output_dir)
                return os.stat(zip_file.name).st_size
            except Exception:
                LOG.error("Failed to extract received ZIP.")
                import traceback
                traceback.print_exc()
                raise
    return 0


def create_review_data(review_status):
    if review_status:
        return ReviewData(status=review_status_enum(review_status.status),
                          comment=review_status.message.decode('utf-8'),
                          author=review_status.author,
                          date=str(review_status.date))
    else:
        return ReviewData(status=ttypes.ReviewStatus.UNREVIEWED)


def create_count_expression(report_filter):
    if report_filter is not None and report_filter.isUnique:
        return func.count(Report.bug_id.distinct())
    else:
        return func.count(literal_column('*'))


def apply_report_filter(q, filter_expression):
    """
    Applies the given filter expression and joins the File and ReviewStatus
    tables.
    """
    q = q.outerjoin(File,
                    Report.file_id == File.id) \
        .outerjoin(ReviewStatus,
                   ReviewStatus.bug_hash == Report.bug_id) \
        .filter(filter_expression)

    return q


def get_sort_map(sort_types, is_unique=False):
    # Get a list of sort_types which will be a nested ORDER BY.
    sort_type_map = {
        SortType.FILENAME: [(File.filepath, 'filepath'),
                            (Report.line, 'line')],
        SortType.BUG_PATH_LENGTH: [(Report.path_length, 'bug_path_length')],
        SortType.CHECKER_NAME: [(Report.checker_id, 'checker_id')],
        SortType.SEVERITY: [(Report.severity, 'severity')],
        SortType.REVIEW_STATUS: [(ReviewStatus.status, 'rw_status')],
        SortType.DETECTION_STATUS: [(Report.detection_status, 'dt_status')]}

    if is_unique:
        sort_type_map[SortType.FILENAME] = [(File.filename, 'filename')]
        sort_type_map[SortType.DETECTION_STATUS] = []

    # Mapping the SQLAlchemy functions.
    order_type_map = {Order.ASC: asc, Order.DESC: desc}

    if sort_types is None:
        sort_types = [SortMode(SortType.SEVERITY, Order.DESC)]

    return sort_types, sort_type_map, order_type_map


def sort_results_query(query, sort_types, sort_type_map, order_type_map,
                       order_by_label=False):
    """
    Helper method for __queryDiffResults and queryResults to apply sorting.
    """
    for sort in sort_types:
        sorttypes = sort_type_map.get(sort.type)
        for sorttype in sorttypes:
            order_type = order_type_map.get(sort.ord)
            sort_col = sorttype[1] if order_by_label else sorttype[0]
            query = query.order_by(order_type(sort_col))

    return query


def filter_unresolved_reports(q):
    """
    Filter reports which are unresolved.

    Note: review status of these reports are not in skip_review_statuses
    and detection statuses are not in skip_detection_statuses.
    """
    skip_review_statuses = ['false_positive', 'intentional']
    skip_detection_statuses = ['resolved', 'off', 'unavailable']

    return q.filter(Report.detection_status.notin_(skip_detection_statuses)) \
            .filter(or_(ReviewStatus.status.is_(None),
                        ReviewStatus.status.notin_(skip_review_statuses))) \
            .outerjoin(ReviewStatus,
                       ReviewStatus.bug_hash == Report.bug_id)


def check_remove_runs_lock(session, run_ids):
    """
    Check if there is an existing lock on the given runs, which has not
    expired yet. If so, the run cannot be deleted, as someone is assumed to
    be storing into it.
    """
    locks_expired_at = datetime.now() - timedelta(
        seconds=db_cleanup.RUN_LOCK_TIMEOUT_IN_DATABASE)

    run_locks = session.query(RunLock.name) \
        .filter(RunLock.locked_at >= locks_expired_at)

    if run_ids:
        run_locks = run_locks.filter(Run.id.in_(run_ids))

    run_locks = run_locks \
        .outerjoin(Run,
                   Run.name == RunLock.name) \
        .all()

    if run_locks:
        raise codechecker_api_shared.ttypes.RequestFailed(
            codechecker_api_shared.ttypes.ErrorCode.DATABASE,
            "Can not remove results because the following runs "
            "are locked: {0}".format(
                ', '.join([r[0] for r in run_locks])))


def sort_run_data_query(query, sort_mode):
    """
    Sort run data query by the given sort type.
    """
    # Sort by run date by default.
    if not sort_mode:
        return query.order_by(desc(Run.date))

    order_type_map = {Order.ASC: asc, Order.DESC: desc}
    order_type = order_type_map.get(sort_mode.ord)
    if sort_mode.type == RunSortType.NAME:
        query = query.order_by(order_type(Run.name))
    elif sort_mode.type == RunSortType.UNRESOLVED_REPORTS:
        query = query.order_by(order_type('report_count'))
    elif sort_mode.type == RunSortType.DATE:
        query = query.order_by(order_type(Run.date))
    elif sort_mode.type == RunSortType.DURATION:
        query = query.order_by(order_type(Run.duration))
    elif sort_mode.type == RunSortType.CC_VERSION:
        query = query.order_by(order_type(RunHistory.cc_version))

    return query


def get_failed_files_query(session, run_ids, query_fields,
                           extra_sub_query_fields=None):
    """
    General function to get query to fetch the list of failed files and to get
    the number of failed files.
    """
    sub_query_fields = [func.max(RunHistory.id).label('history_id')]
    if extra_sub_query_fields:
        sub_query_fields.extend(extra_sub_query_fields)

    sub_q = session.query(*sub_query_fields)

    if run_ids:
        sub_q = sub_q.filter(RunHistory.run_id.in_(run_ids))

    sub_q = sub_q \
        .group_by(RunHistory.run_id) \
        .subquery()

    query = session \
        .query(*query_fields) \
        .outerjoin(sub_q,
                   AnalyzerStatistic.run_history_id == sub_q.c.history_id) \
        .filter(AnalyzerStatistic.run_history_id == sub_q.c.history_id)

    return query, sub_q


def get_analysis_statistics_query(session, run_ids, run_history_ids=None):
    """ Get analyzer statistics query. """
    query = session.query(AnalyzerStatistic, Run.id)

    if run_ids:
        # Subquery to get analyzer statistics only for these run history id's.
        history_ids_subq = session.query(
                func.max(AnalyzerStatistic.run_history_id)) \
            .filter(RunHistory.run_id.in_(run_ids)) \
            .outerjoin(
                RunHistory,
                RunHistory.id == AnalyzerStatistic.run_history_id) \
            .group_by(RunHistory.run_id) \
            .subquery()

        query = query.filter(
            AnalyzerStatistic.run_history_id.in_(history_ids_subq))
    elif run_history_ids:
        query = query.filter(RunHistory.id.in_(run_history_ids))

    return query \
        .outerjoin(RunHistory,
                   RunHistory.id == AnalyzerStatistic.run_history_id) \
        .outerjoin(Run,
                   Run.id == RunHistory.run_id)


class ThriftRequestHandler(object):
    """
    Connect to database and handle thrift client requests.
    """

    def __init__(self,
                 manager,
                 Session,
                 product,
                 auth_session,
                 config_database,
                 checker_md_docs,
                 checker_md_docs_map,
                 package_version,
                 context):

        if not product:
            raise ValueError("Cannot initialize request handler without "
                             "a product to serve.")

        self.__manager = manager
        self.__product = product
        self.__auth_session = auth_session
        self.__config_database = config_database
        self.__checker_md_docs = checker_md_docs
        self.__checker_doc_map = checker_md_docs_map
        self.__package_version = package_version
        self.__Session = Session
        self.__context = context
        self.__permission_args = {
            'productID': product.id
        }

    def __get_username(self):
        """
        Returns the actually logged in user name.
        """
        return self.__auth_session.user if self.__auth_session else "Anonymous"

    def __require_permission(self, required):
        """
        Helper method to raise an UNAUTHORIZED exception if the user does not
        have any of the given permissions.
        """

        with DBSession(self.__config_database) as session:
            args = dict(self.__permission_args)
            args['config_db_session'] = session

            if not any([permissions.require_permission(
                    perm, args, self.__auth_session)
                    for perm in required]):
                raise codechecker_api_shared.ttypes.RequestFailed(
                    codechecker_api_shared.ttypes.ErrorCode.UNAUTHORIZED,
                    "You are not authorized to execute this action.")

            return True

    def __require_admin(self):
        self.__require_permission([permissions.PRODUCT_ADMIN])

    def __require_access(self):
        self.__require_permission([permissions.PRODUCT_ACCESS])

    def __require_store(self):
        self.__require_permission([permissions.PRODUCT_STORE])

    def __add_comment(self, bug_id, message, kind=CommentKindValue.USER,
                      date=None):
        """ Creates a new comment object. """
        user = self.__get_username()
        return Comment(bug_id,
                       user,
                       message.encode('utf-8'),
                       kind,
                       date or datetime.now())

    @timeit
    def getRunData(self, run_filter, limit, offset, sort_mode):
        self.__require_access()

        limit = verify_limit_range(limit)

        with DBSession(self.__Session) as session:

            # Count the reports subquery.
            stmt = session.query(Report.run_id,
                                 func.count(Report.bug_id)
                                 .label('report_count'))

            stmt = filter_unresolved_reports(stmt) \
                .group_by(Report.run_id).subquery()

            tag_q = session.query(RunHistory.run_id,
                                  func.max(RunHistory.id).label(
                                      'run_history_id'),
                                  func.max(RunHistory.time).label(
                                      'run_history_time')) \
                .group_by(RunHistory.run_id) \
                .subquery()

            q = session.query(Run.id,
                              Run.date,
                              Run.name,
                              Run.duration,
                              RunHistory.version_tag,
                              RunHistory.cc_version,
                              RunHistory.description,
                              stmt.c.report_count)

            q = process_run_filter(session, q, run_filter)

            q = q.outerjoin(stmt, Run.id == stmt.c.run_id) \
                .outerjoin(tag_q, Run.id == tag_q.c.run_id) \
                .outerjoin(RunHistory,
                           RunHistory.id == tag_q.c.run_history_id) \
                .group_by(Run.id,
                          RunHistory.version_tag,
                          RunHistory.cc_version,
                          RunHistory.description,
                          stmt.c.report_count)

            q = sort_run_data_query(q, sort_mode)

            if limit:
                q = q.limit(limit).offset(offset)

            # Get the runs.
            run_data = q.all()

            # Set run ids filter by using the previous results.
            if not run_filter:
                run_filter = RunFilter()

            run_filter.ids = [r[0] for r in run_data]

            # Get report count for each detection statuses.
            status_q = session.query(Report.run_id,
                                     Report.detection_status,
                                     func.count(Report.bug_id)) \
                .filter(Report.run_id.in_(run_filter.ids)) \
                .group_by(Report.run_id, Report.detection_status)

            status_sum = defaultdict(defaultdict)
            for run_id, status, count in status_q:
                status_sum[run_id][detection_status_enum(status)] = count

            # Get analyzer statistics.
            analyzer_statistics = defaultdict(lambda: defaultdict())

            stat_q = get_analysis_statistics_query(session, run_filter.ids)
            for stat, run_id in stat_q:
                analyzer_statistics[run_id][stat.analyzer_type] = \
                    ttypes.AnalyzerStatistics(failed=stat.failed,
                                              successful=stat.successful)

            results = []

            for run_id, run_date, run_name, duration, tag, cc_version, \
                description, report_count \
                    in run_data:

                if report_count is None:
                    report_count = 0

                analyzer_stats = analyzer_statistics[run_id]
                results.append(RunData(runId=run_id,
                                       runDate=str(run_date),
                                       name=run_name,
                                       duration=duration,
                                       resultCount=report_count,
                                       detectionStatusCount=status_sum[run_id],
                                       versionTag=tag,
                                       codeCheckerVersion=cc_version,
                                       analyzerStatistics=analyzer_stats,
                                       description=description))
            return results

    @exc_to_thrift_reqfail
    @timeit
    def getRunCount(self, run_filter):
        self.__require_access()

        with DBSession(self.__Session) as session:
            query = session.query(Run.id)
            query = process_run_filter(session, query, run_filter)

        return query.count()

    def getCheckCommand(self, run_history_id, run_id):
        self.__require_access()

        if not run_history_id and not run_id:
            return ""

        with DBSession(self.__Session) as session:
            query = session.query(RunHistory.check_command)

            if run_history_id:
                query = query.filter(RunHistory.id == run_history_id)
            elif run_id:
                query = query.filter(RunHistory.run_id == run_id) \
                    .order_by(RunHistory.time.desc()) \
                    .limit(1)

            history = query.first()

            if not history or not history[0]:
                return ""

        return zlib.decompress(history[0]).decode('utf-8')

    @exc_to_thrift_reqfail
    @timeit
    def getRunHistory(self, run_ids, limit, offset, run_history_filter):
        self.__require_access()

        limit = verify_limit_range(limit)

        with DBSession(self.__Session) as session:

            res = session.query(RunHistory)

            res = process_run_history_filter(res, run_ids, run_history_filter)

            res = res.order_by(RunHistory.time.desc())

            if limit:
                res = res.limit(limit).offset(offset)

            results = []
            for history in res:
                analyzer_statistics = {}
                for stat in history.analyzer_statistics:
                    analyzer_statistics[stat.analyzer_type] = \
                        ttypes.AnalyzerStatistics(
                            failed=stat.failed,
                            successful=stat.successful)

                results.append(RunHistoryData(
                    id=history.id,
                    runId=history.run.id,
                    runName=history.run.name,
                    versionTag=history.version_tag,
                    user=history.user,
                    time=str(history.time),
                    codeCheckerVersion=history.cc_version,
                    analyzerStatistics=analyzer_statistics,
                    description=history.description))

            return results

    @exc_to_thrift_reqfail
    @timeit
    def getRunHistoryCount(self, run_ids, run_history_filter):
        self.__require_access()

        with DBSession(self.__Session) as session:
            query = session.query(RunHistory.id)
            query = process_run_history_filter(query,
                                               run_ids,
                                               run_history_filter)

        return query.count()

    @exc_to_thrift_reqfail
    @timeit
    def getReport(self, reportId):
        self.__require_access()

        with DBSession(self.__Session) as session:

            result = session.query(Report,
                                   File,
                                   ReviewStatus) \
                .filter(Report.id == reportId) \
                .outerjoin(File, Report.file_id == File.id) \
                .outerjoin(ReviewStatus,
                           ReviewStatus.bug_hash == Report.bug_id) \
                .limit(1).one_or_none()

            if not result:
                raise codechecker_api_shared.ttypes.RequestFailed(
                    codechecker_api_shared.ttypes.ErrorCode.DATABASE,
                    "Report " + str(reportId) + " not found!")

            report, source_file, review_status = result
            return ReportData(
                runId=report.run_id,
                bugHash=report.bug_id,
                checkedFile=source_file.filepath,
                checkerMsg=report.checker_message,
                reportId=report.id,
                fileId=source_file.id,
                line=report.line,
                column=report.column,
                checkerId=report.checker_id,
                severity=report.severity,
                reviewData=create_review_data(review_status),
                detectionStatus=detection_status_enum(report.detection_status),
                detectedAt=str(report.detected_at),
                fixedAt=str(report.fixed_at) if report.fixed_at else None)

    @exc_to_thrift_reqfail
    @timeit
    def getDiffResultsHash(self, run_ids, report_hashes, diff_type,
                           skip_detection_statuses, tag_ids):
        self.__require_access()

        if not skip_detection_statuses:
            skip_detection_statuses = [ttypes.DetectionStatus.RESOLVED,
                                       ttypes.DetectionStatus.OFF,
                                       ttypes.DetectionStatus.UNAVAILABLE]

        # Convert statuses to string.
        skip_statuses_str = [detection_status_str(status)
                             for status in skip_detection_statuses]

        with DBSession(self.__Session) as session:
            if diff_type == DiffType.NEW:
                # In postgresql we can select multiple rows filled with
                # constants by using `unnest` function. In sqlite we have to
                # use multiple UNION ALL.

                if not report_hashes:
                    return []

                base_hashes = session.query(Report.bug_id.label('bug_id')) \
                    .outerjoin(File, Report.file_id == File.id) \
                    .filter(Report.detection_status.notin_(skip_statuses_str))

                base_hashes = \
                    filter_open_reports_in_tags(base_hashes, run_ids, tag_ids)

                if self.__product.driver_name == 'postgresql':
                    new_hashes = select([func.unnest(report_hashes)
                                         .label('bug_id')]) \
                        .except_(base_hashes).alias('new_bugs')
                    return [res[0] for res in session.query(new_hashes)]
                else:
                    # The maximum number of compound select in sqlite is 500
                    # by default. We increased SQLITE_MAX_COMPOUND_SELECT
                    # limit but when the number of compound select was larger
                    # than 8435 sqlite threw a `Segmentation fault` error.
                    # For this reason we create queries with chunks.
                    new_hashes = []
                    chunk_size = 500
                    for chunk in util.chunks(iter(report_hashes), chunk_size):
                        new_hashes_query = union_all(*[
                            select([bindparam('bug_id' + str(i), h)
                                    .label('bug_id')])
                            for i, h in enumerate(chunk)])
                        q = select([new_hashes_query]).except_(base_hashes)
                        new_hashes.extend([res[0] for res in session.query(q)])

                    return new_hashes
            elif diff_type == DiffType.RESOLVED:
                results = session.query(Report.bug_id) \
                    .filter(Report.bug_id.notin_(report_hashes))

                results = \
                    filter_open_reports_in_tags(results, run_ids, tag_ids)

                return [res[0] for res in results]

            elif diff_type == DiffType.UNRESOLVED:
                results = session.query(Report.bug_id) \
                    .filter(Report.bug_id.in_(report_hashes)) \
                    .filter(Report.detection_status.notin_(skip_statuses_str))

                results = \
                    filter_open_reports_in_tags(results, run_ids, tag_ids)

                return [res[0] for res in results]

            else:
                return []

    @exc_to_thrift_reqfail
    @timeit
    def getRunResults(self, run_ids, limit, offset, sort_types,
                      report_filter, cmp_data, get_details):
        self.__require_access()

        limit = verify_limit_range(limit)

        with DBSession(self.__Session) as session:
            results = []

            filter_expression = process_report_filter(session, run_ids,
                                                      report_filter, cmp_data)

            is_unique = report_filter is not None and report_filter.isUnique
            if is_unique:
                sort_types, sort_type_map, order_type_map = \
                    get_sort_map(sort_types, True)

                selects = [func.max(Report.id).label('id')]
                for sort in sort_types:
                    sorttypes = sort_type_map.get(sort.type)
                    for sorttype in sorttypes:
                        if sorttype[0] != 'bug_path_length':
                            selects.append(func.max(sorttype[0])
                                           .label(sorttype[1]))

                unique_reports = session.query(*selects)
                unique_reports = apply_report_filter(unique_reports,
                                                     filter_expression)
                unique_reports = unique_reports \
                    .group_by(Report.bug_id) \
                    .subquery()

                # Sort the results
                sorted_reports = \
                    session.query(unique_reports.c.id)

                sorted_reports = sort_results_query(sorted_reports,
                                                    sort_types,
                                                    sort_type_map,
                                                    order_type_map,
                                                    True)

                sorted_reports = sorted_reports \
                    .limit(limit).offset(offset).subquery()

                q = session.query(Report.id, Report.bug_id,
                                  Report.checker_message, Report.checker_id,
                                  Report.severity, Report.detected_at,
                                  Report.fixed_at, ReviewStatus,
                                  File.filename, File.filepath,
                                  Report.path_length, Report.analyzer_name) \
                    .outerjoin(File, Report.file_id == File.id) \
                    .outerjoin(ReviewStatus,
                               ReviewStatus.bug_hash == Report.bug_id) \
                    .outerjoin(sorted_reports,
                               sorted_reports.c.id == Report.id) \
                    .filter(sorted_reports.c.id.isnot(None))

                # We have to sort the results again because an ORDER BY in a
                # subtable is broken by the JOIN.
                q = sort_results_query(q,
                                       sort_types,
                                       sort_type_map,
                                       order_type_map)

                query_result = q.all()

                # Get report details if it is required.
                report_details = {}
                if get_details:
                    report_ids = [r[0] for r in query_result]
                    report_details = get_report_details(session, report_ids)

                for report_id, bug_id, checker_msg, checker, severity, \
                    detected_at, fixed_at, status, filename, path, \
                        bug_path_len, analyzer_name in query_result:
                    review_data = create_review_data(status)

                    results.append(
                        ReportData(bugHash=bug_id,
                                   checkedFile=filename,
                                   checkerMsg=checker_msg,
                                   checkerId=checker,
                                   severity=severity,
                                   reviewData=review_data,
                                   detectedAt=str(detected_at),
                                   fixedAt=str(fixed_at),
                                   bugPathLength=bug_path_len,
                                   details=report_details.get(report_id),
                                   analyzerName=analyzer_name))
            else:
                q = session.query(Report.run_id, Report.id, Report.file_id,
                                  Report.line, Report.column,
                                  Report.detection_status, Report.bug_id,
                                  Report.checker_message, Report.checker_id,
                                  Report.severity, Report.detected_at,
                                  Report.fixed_at, ReviewStatus,
                                  File.filepath,
                                  Report.path_length, Report.analyzer_name) \
                    .outerjoin(File, Report.file_id == File.id) \
                    .outerjoin(ReviewStatus,
                               ReviewStatus.bug_hash == Report.bug_id) \
                    .filter(filter_expression)

                sort_types, sort_type_map, order_type_map = \
                    get_sort_map(sort_types)

                q = sort_results_query(q, sort_types, sort_type_map,
                                       order_type_map)

                q = q.limit(limit).offset(offset)

                query_result = q.all()

                # Get report details if it is required.
                report_details = {}
                if get_details:
                    report_ids = [r[1] for r in query_result]
                    report_details = get_report_details(session, report_ids)

                for run_id, report_id, file_id, line, column, d_status, \
                    bug_id, checker_msg, checker, severity, detected_at,\
                    fixed_at, r_status, path, bug_path_len, analyzer_name \
                        in query_result:

                    review_data = create_review_data(r_status)
                    results.append(
                        ReportData(runId=run_id,
                                   bugHash=bug_id,
                                   checkedFile=path,
                                   checkerMsg=checker_msg,
                                   reportId=report_id,
                                   fileId=file_id,
                                   line=line,
                                   column=column,
                                   checkerId=checker,
                                   severity=severity,
                                   reviewData=review_data,
                                   detectionStatus=detection_status_enum(
                                       d_status),
                                   detectedAt=str(detected_at),
                                   fixedAt=str(fixed_at) if fixed_at else None,
                                   bugPathLength=bug_path_len,
                                   details=report_details.get(report_id),
                                   analyzerName=analyzer_name))

            return results

    @timeit
    def getRunReportCounts(self, run_ids, report_filter, limit, offset):
        """
          Count the results separately for multiple runs.
          If an empty run id list is provided the report
          counts will be calculated for all of the available runs.
        """
        self.__require_access()

        limit = verify_limit_range(limit)

        results = []
        with DBSession(self.__Session) as session:
            filter_expression = process_report_filter(session, run_ids,
                                                      report_filter)

            count_expr = create_count_expression(report_filter)
            q = session.query(Run.id,
                              Run.name,
                              count_expr) \
                .select_from(Report)

            q = q.outerjoin(File, Report.file_id == File.id) \
                .outerjoin(ReviewStatus,
                           ReviewStatus.bug_hash == Report.bug_id) \
                .outerjoin(Run,
                           Report.run_id == Run.id) \
                .filter(filter_expression) \
                .order_by(Run.name) \
                .group_by(Run.id)

            if limit:
                q = q.limit(limit).offset(offset)

            for run_id, run_name, count in q:
                report_count = RunReportCount(runId=run_id,
                                              name=run_name,
                                              reportCount=count)
                results.append(report_count)

            return results

    @exc_to_thrift_reqfail
    @timeit
    def getRunResultCount(self, run_ids, report_filter, cmp_data):
        self.__require_access()

        with DBSession(self.__Session) as session:
            filter_expression = process_report_filter(session, run_ids,
                                                      report_filter, cmp_data)

            q = session.query(Report.bug_id)
            q = apply_report_filter(q, filter_expression)

            if report_filter is not None and report_filter.isUnique:
                q = q.group_by(Report.bug_id)

            report_count = q.count()
            if report_count is None:
                report_count = 0

            return report_count

    @staticmethod
    @timeit
    def __construct_bug_item_list(session, report_id, item_type):

        q = session.query(item_type) \
            .filter(item_type.report_id == report_id) \
            .order_by(item_type.order)

        bug_items = []

        for event in q:
            f = session.query(File).get(event.file_id)
            bug_items.append((event, f.filepath))

        return bug_items

    @exc_to_thrift_reqfail
    @timeit
    def getReportDetails(self, reportId):
        """
        Parameters:
         - reportId
        """
        self.__require_access()
        with DBSession(self.__Session) as session:
            return get_report_details(session, [reportId])[reportId]

    def _setReviewStatus(self, session, report_hash, status,
                         message, date=None):
        """
        This function sets the review status of the given report. This is the
        implementation of changeReviewStatus(), but it is also extended with
        a session parameter which represents a database transaction. This is
        needed because during storage a specific session object has to be used.
        """
        review_status = session.query(ReviewStatus).get(report_hash)
        if review_status is None:
            review_status = ReviewStatus()
            review_status.bug_hash = report_hash

        old_status = review_status.status or \
            review_status_str(ttypes.ReviewStatus.UNREVIEWED)
        old_msg = review_status.message or None

        new_status = review_status_str(status)
        new_user = self.__get_username()
        new_message = message.encode('utf8') if message else b''

        # Review status is a shared table among runs. When multiple runs
        # are stored in parallel, there may be a race condition in updating
        # review status fields. The most common reason of deadlocks is
        # changing only the date to current date. This condition checks if
        # something else is also changed other than dates.
        # We assume that report status in source code comments belong to
        # the first user who stored the reports. If another user stores the
        # same project with same report status then we don't change it.
        if (old_status, old_msg) == (new_status, new_message):
            return True

        review_status.status = new_status
        review_status.author = new_user
        review_status.message = new_message
        review_status.date = date or datetime.now()
        session.add(review_status)

        # Create a system comment if the review status or the message
        # is changed.
        old_review_status = old_status.capitalize()
        new_review_status = review_status.status.capitalize()
        if message:
            system_comment_msg = \
                'rev_st_changed_msg {0} {1} {2}'.format(
                    old_review_status, new_review_status,
                    shlex.quote(message))
        else:
            system_comment_msg = 'rev_st_changed {0} {1}'.format(
                old_review_status, new_review_status)

        system_comment = self.__add_comment(review_status.bug_hash,
                                            system_comment_msg,
                                            CommentKindValue.SYSTEM,
                                            review_status.date)
        session.add(system_comment)

        session.flush()

        return True

    @exc_to_thrift_reqfail
    @timeit
    def isReviewStatusChangeDisabled(self):
        """
        Return True if review status change is disabled.
        """
        with DBSession(self.__config_database) as session:
            product = session.query(Product).get(self.__product.id)
            return product.is_review_status_change_disabled

    @exc_to_thrift_reqfail
    @timeit
    def changeReviewStatus(self, report_id, status, message):
        """
        Change review status of the bug by report id.
        """
        self.__require_permission([permissions.PRODUCT_ACCESS,
                                   permissions.PRODUCT_STORE])

        if self.isReviewStatusChangeDisabled():
            msg = "Review status change is disabled!"
            raise codechecker_api_shared.ttypes.RequestFailed(
                codechecker_api_shared.ttypes.ErrorCode.GENERAL, msg)

        with DBSession(self.__Session) as session:
            report = session.query(Report).get(report_id)
            if report:
                res = self._setReviewStatus(
                    session, report.bug_id, status, message)
            else:
                raise codechecker_api_shared.ttypes.RequestFailed(
                    codechecker_api_shared.ttypes.ErrorCode.DATABASE,
                    "No report found in the database.")
            session.commit()

            LOG.info("Review status of report '%s' was changed to '%s' by %s.",
                     report_id, review_status_str(status),
                     self.__get_username())

        return res

    @exc_to_thrift_reqfail
    @timeit
    def getComments(self, report_id):
        """
            Return the list of comments for the given bug.
        """
        self.__require_access()

        with DBSession(self.__Session) as session:
            report = session.query(Report).get(report_id)
            if report:
                result = []

                comments = session.query(Comment) \
                    .filter(Comment.bug_hash == report.bug_id) \
                    .order_by(Comment.created_at.desc()) \
                    .all()

                for comment in comments:
                    message = get_comment_msg(comment)
                    result.append(CommentData(
                        comment.id,
                        comment.author,
                        message,
                        str(comment.created_at),
                        comment_kind_to_thrift_type(comment.kind)))

                return result
            else:
                msg = 'Report id ' + str(report_id) + \
                      ' was not found in the database.'
                raise codechecker_api_shared.ttypes.RequestFailed(
                    codechecker_api_shared.ttypes.ErrorCode.DATABASE, msg)

    @exc_to_thrift_reqfail
    @timeit
    def getCommentCount(self, report_id):
        """
            Return the number of comments for the given bug.
        """
        self.__require_access()
        with DBSession(self.__Session) as session:
            report = session.query(Report).get(report_id)
            if report:
                commentCount = session.query(Comment) \
                    .filter(Comment.bug_hash == report.bug_id) \
                    .count()

            if commentCount is None:
                commentCount = 0

            return commentCount

    @exc_to_thrift_reqfail
    @timeit
    def addComment(self, report_id, comment_data):
        """ Add new comment for the given bug. """
        self.__require_access()

        if not comment_data.message.strip():
            raise codechecker_api_shared.ttypes.RequestFailed(
                codechecker_api_shared.ttypes.ErrorCode.GENERAL,
                'The comment message can not be empty!')

        with DBSession(self.__Session) as session:
            report = session.query(Report).get(report_id)
            if report:
                comment = self.__add_comment(report.bug_id,
                                             comment_data.message)
                session.add(comment)
                session.commit()

                return True
            else:
                msg = 'Report id ' + str(report_id) + \
                      ' was not found in the database.'
                LOG.error(msg)
                raise codechecker_api_shared.ttypes.RequestFailed(
                    codechecker_api_shared.ttypes.ErrorCode.DATABASE, msg)

    @exc_to_thrift_reqfail
    @timeit
    def updateComment(self, comment_id, content):
        """
            Update the given comment message with new content. We allow
            comments to be updated by it's original author only, except for
            Anyonymous comments that can be updated by anybody.
        """
        self.__require_access()

        if not content.strip():
            raise codechecker_api_shared.ttypes.RequestFailed(
                codechecker_api_shared.ttypes.ErrorCode.GENERAL,
                'The comment message can not be empty!')

        with DBSession(self.__Session) as session:

            user = self.__get_username()

            comment = session.query(Comment).get(comment_id)
            if comment:
                if comment.author != 'Anonymous' and comment.author != user:
                    raise codechecker_api_shared.ttypes.RequestFailed(
                        codechecker_api_shared.ttypes.ErrorCode.UNAUTHORIZED,
                        'Unathorized comment modification!')

                # Create system comment if the message is changed.
                message = comment.message.decode('utf-8')
                if message != content:
                    system_comment_msg = 'comment_changed {0} {1}'.format(
                        shlex.quote(message),
                        shlex.quote(content))

                    system_comment = \
                        self.__add_comment(comment.bug_hash,
                                           system_comment_msg,
                                           CommentKindValue.SYSTEM)
                    session.add(system_comment)

                comment.message = content.encode('utf-8')
                session.add(comment)

                session.commit()
                return True
            else:
                msg = 'Comment id ' + str(comment_id) + \
                      ' was not found in the database.'
                LOG.error(msg)
                raise codechecker_api_shared.ttypes.RequestFailed(
                    codechecker_api_shared.ttypes.ErrorCode.DATABASE, msg)

    @exc_to_thrift_reqfail
    @timeit
    def removeComment(self, comment_id):
        """
            Remove the comment. We allow comments to be removed by it's
            original author only, except for Anyonymous comments that can be
            updated by anybody.
        """
        self.__require_access()

        user = self.__get_username()

        with DBSession(self.__Session) as session:

            comment = session.query(Comment).get(comment_id)
            if comment:
                if comment.author != 'Anonymous' and comment.author != user:
                    raise codechecker_api_shared.ttypes.RequestFailed(
                        codechecker_api_shared.ttypes.ErrorCode.UNAUTHORIZED,
                        'Unathorized comment modification!')
                session.delete(comment)
                session.commit()

                LOG.info("Comment '%s...' was removed from bug hash '%s' by "
                         "'%s'.", comment.message[:10], comment.bug_hash,
                         self.__get_username())

                return True
            else:
                msg = 'Comment id ' + str(comment_id) + \
                      ' was not found in the database.'
                raise codechecker_api_shared.ttypes.RequestFailed(
                    codechecker_api_shared.ttypes.ErrorCode.DATABASE, msg)

    @exc_to_thrift_reqfail
    @timeit
    def getCheckerDoc(self, checkerId):
        """
        Parameters:
         - checkerId
        """

        missing_doc = "No documentation found for checker: " + checkerId + \
                      "\n\nPlease refer to the documentation at the "

        if "." in checkerId:
            sa_link = "http://clang-analyzer.llvm.org/available_checks.html"
            missing_doc += "[ClangSA](" + sa_link + ")"
        elif "-" in checkerId:
            tidy_link = "http://clang.llvm.org/extra/clang-tidy/checks/" + \
                checkerId + ".html"
            missing_doc += "[ClangTidy](" + tidy_link + ")"
        missing_doc += " homepage."

        try:
            md_file = self.__checker_doc_map.get(checkerId)
            if md_file:
                md_file = os.path.join(self.__checker_md_docs, md_file)
                try:
                    with open(md_file, 'r',
                              encoding='utf-8',
                              errors='ignore') as md_content:
                        missing_doc = md_content.read()
                except (IOError, OSError):
                    LOG.warning("Failed to read checker documentation: %s",
                                md_file)

            return missing_doc

        except Exception as ex:
            msg = str(ex)
            raise codechecker_api_shared.ttypes.RequestFailed(
                codechecker_api_shared.ttypes.ErrorCode.IOERROR, msg)

    @exc_to_thrift_reqfail
    @timeit
    def getSourceFileData(self, fileId, fileContent, encoding):
        """
        Parameters:
         - fileId
         - fileContent
         - enum Encoding
        """
        self.__require_access()
        with DBSession(self.__Session) as session:
            sourcefile = session.query(File).get(fileId)

            if sourcefile is None:
                return SourceFileData()

            if fileContent:
                cont = session.query(FileContent).get(sourcefile.content_hash)
                source = zlib.decompress(cont.content)

                if encoding == Encoding.BASE64:
                    source = base64.b64encode(source)

                source = source.decode('utf-8', errors='ignore')
                return SourceFileData(fileId=sourcefile.id,
                                      filePath=sourcefile.filepath,
                                      fileContent=source)
            else:
                return SourceFileData(fileId=sourcefile.id,
                                      filePath=sourcefile.filepath)

    @exc_to_thrift_reqfail
    @timeit
    def getLinesInSourceFileContents(self, lines_in_files_requested, encoding):
        self.__require_access()
        with DBSession(self.__Session) as session:
            res = defaultdict(lambda: defaultdict(str))
            for lines_in_file in lines_in_files_requested:
                if lines_in_file.fileId is None:
                    LOG.warning("File content requested without a fileId.")
                    LOG.warning(lines_in_file)
                    continue
                sourcefile = session.query(File).get(lines_in_file.fileId)
                cont = session.query(FileContent).get(sourcefile.content_hash)
                lines = zlib.decompress(
                    cont.content).decode('utf-8', 'ignore').split('\n')
                for line in lines_in_file.lines:
                    content = '' if len(lines) < line else lines[line - 1]
                    if encoding == Encoding.BASE64:
                        content = convert.to_b64(content)
                    res[lines_in_file.fileId][line] = content
            return res

    @exc_to_thrift_reqfail
    @timeit
    def getCheckerCounts(self, run_ids, report_filter, cmp_data, limit,
                         offset):
        """
          If the run id list is empty the metrics will be counted
          for all of the runs and in compare mode all of the runs
          will be used as a baseline excluding the runs in compare data.
        """
        self.__require_access()

        limit = verify_limit_range(limit)

        results = []
        with DBSession(self.__Session) as session:
            filter_expression = process_report_filter(session, run_ids,
                                                      report_filter, cmp_data)

            is_unique = report_filter is not None and report_filter.isUnique
            if is_unique:
                q = session.query(func.max(Report.checker_id).label(
                    'checker_id'),
                    func.max(Report.severity).label(
                    'severity'),
                    Report.bug_id)
            else:
                q = session.query(Report.checker_id,
                                  Report.severity,
                                  func.count(Report.id))

            q = apply_report_filter(q, filter_expression)

            if is_unique:
                q = q.group_by(Report.bug_id).subquery()
                unique_checker_q = session.query(q.c.checker_id,
                                                 func.max(q.c.severity),
                                                 func.count(q.c.bug_id)) \
                    .group_by(q.c.checker_id) \
                    .order_by(q.c.checker_id)
            else:
                unique_checker_q = q.group_by(Report.checker_id,
                                              Report.severity) \
                    .order_by(Report.checker_id)

            if limit:
                unique_checker_q = unique_checker_q.limit(limit).offset(offset)

            for name, severity, count in unique_checker_q:
                checker_count = CheckerCount(name=name,
                                             severity=severity,
                                             count=count)
                results.append(checker_count)
        return results

    @exc_to_thrift_reqfail
    @timeit
    def getAnalyzerNameCounts(self, run_ids, report_filter, cmp_data, limit,
                              offset):
        """
          If the run id list is empty the metrics will be counted
          for all of the runs and in compare mode all of the runs
          will be used as a baseline excluding the runs in compare data.
        """
        self.__require_access()

        limit = verify_limit_range(limit)

        results = {}
        with DBSession(self.__Session) as session:
            filter_expression = process_report_filter(session, run_ids,
                                                      report_filter, cmp_data)

            is_unique = report_filter is not None and report_filter.isUnique
            if is_unique:
                q = session.query(func.max(Report.analyzer_name).label(
                    'analyzer_name'),
                    Report.bug_id)
            else:
                q = session.query(Report.analyzer_name,
                                  func.count(Report.id))

            q = apply_report_filter(q, filter_expression)

            if is_unique:
                q = q.group_by(Report.bug_id).subquery()
                analyzer_name_q = session.query(q.c.analyzer_name,
                                                func.count(q.c.bug_id)) \
                    .group_by(q.c.analyzer_name) \
                    .order_by(q.c.analyzer_name)
            else:
                analyzer_name_q = q.group_by(Report.analyzer_name) \
                    .order_by(Report.analyzer_name)

            if limit:
                analyzer_name_q = analyzer_name_q.limit(limit).offset(offset)

            for name, count in analyzer_name_q:
                results[name] = count

        return results

    @exc_to_thrift_reqfail
    @timeit
    def getSeverityCounts(self, run_ids, report_filter, cmp_data):
        """
          If the run id list is empty the metrics will be counted
          for all of the runs and in compare mode all of the runs
          will be used as a baseline excluding the runs in compare data.
        """
        self.__require_access()
        results = {}
        with DBSession(self.__Session) as session:
            filter_expression = process_report_filter(session, run_ids,
                                                      report_filter, cmp_data)

            is_unique = report_filter is not None and report_filter.isUnique
            if is_unique:
                q = session.query(func.max(Report.severity).label('severity'),
                                  Report.bug_id)
            else:
                q = session.query(Report.severity,
                                  func.count(Report.id))

            q = apply_report_filter(q, filter_expression)

            if is_unique:
                q = q.group_by(Report.bug_id).subquery()
                severities = session.query(q.c.severity,
                                           func.count(q.c.bug_id)) \
                    .group_by(q.c.severity)
            else:
                severities = q.group_by(Report.severity)

            results = dict(severities)
        return results

    @exc_to_thrift_reqfail
    @timeit
    def getCheckerMsgCounts(self, run_ids, report_filter, cmp_data, limit,
                            offset):
        """
          If the run id list is empty the metrics will be counted
          for all of the runs and in compare mode all of the runs
          will be used as a baseline excluding the runs in compare data.
        """
        self.__require_access()

        limit = verify_limit_range(limit)

        results = {}
        with DBSession(self.__Session) as session:
            filter_expression = process_report_filter(session, run_ids,
                                                      report_filter, cmp_data)

            is_unique = report_filter is not None and report_filter.isUnique
            if is_unique:
                q = session.query(func.max(Report.checker_message).label(
                    'checker_message'),
                    Report.bug_id)
            else:
                q = session.query(Report.checker_message,
                                  func.count(Report.id))

            q = apply_report_filter(q, filter_expression)

            if is_unique:
                q = q.group_by(Report.bug_id).subquery()
                checker_messages = session.query(q.c.checker_message,
                                                 func.count(q.c.bug_id)) \
                    .group_by(q.c.checker_message) \
                    .order_by(q.c.checker_message)
            else:
                checker_messages = q.group_by(Report.checker_message) \
                                    .order_by(Report.checker_message)

            if limit:
                checker_messages = checker_messages.limit(limit).offset(offset)

            results = dict(checker_messages.all())
        return results

    @exc_to_thrift_reqfail
    @timeit
    def getReviewStatusCounts(self, run_ids, report_filter, cmp_data):
        """
          If the run id list is empty the metrics will be counted
          for all of the runs and in compare mode all of the runs
          will be used as a baseline excluding the runs in compare data.
        """
        self.__require_access()
        results = defaultdict(int)
        with DBSession(self.__Session) as session:
            filter_expression = process_report_filter(session, run_ids,
                                                      report_filter, cmp_data)

            is_unique = report_filter is not None and report_filter.isUnique
            if is_unique:
                q = session.query(Report.bug_id,
                                  func.max(ReviewStatus.status).label(
                                      'status'))
            else:
                q = session.query(func.max(Report.bug_id),
                                  ReviewStatus.status,
                                  func.count(Report.id))

            q = apply_report_filter(q, filter_expression)

            if is_unique:
                q = q.group_by(Report.bug_id).subquery()
                review_statuses = session.query(func.max(q.c.bug_id),
                                                q.c.status,
                                                func.count(q.c.bug_id)) \
                    .group_by(q.c.status)
            else:
                review_statuses = q.group_by(ReviewStatus.status)

            for _, rev_status, count in review_statuses:
                if rev_status is None:
                    # If no review status is set count it as unreviewed.
                    rev_status = ttypes.ReviewStatus.UNREVIEWED
                    results[rev_status] += count
                else:
                    rev_status = review_status_enum(rev_status)
                    results[rev_status] += count
        return results

    @exc_to_thrift_reqfail
    @timeit
    def getFileCounts(self, run_ids, report_filter, cmp_data, limit, offset):
        """
          If the run id list is empty the metrics will be counted
          for all of the runs and in compare mode all of the runs
          will be used as a baseline excluding the runs in compare data.
        """
        self.__require_access()

        limit = verify_limit_range(limit)

        results = {}
        with DBSession(self.__Session) as session:
            filter_expression = process_report_filter(session, run_ids,
                                                      report_filter, cmp_data)

            stmt = session.query(Report.bug_id,
                                 Report.file_id)
            stmt = apply_report_filter(stmt, filter_expression)

            if report_filter is not None and report_filter.isUnique:
                stmt = stmt.group_by(Report.bug_id, Report.file_id)

            stmt = stmt.subquery()

            # When using pg8000, 1 cannot be passed as parameter to the count
            # function. This is the reason why we have to convert it to
            # Integer (see: https://github.com/mfenniak/pg8000/issues/110)
            count_int = cast(1, sqlalchemy.Integer)
            report_count = session.query(stmt.c.file_id,
                                         func.count(count_int).label(
                                             'report_count')) \
                .group_by(stmt.c.file_id)

            if limit:
                report_count = report_count.limit(limit).offset(offset)

            report_count = report_count.subquery()
            file_paths = session.query(File.filepath,
                                       report_count.c.report_count) \
                .join(report_count,
                      report_count.c.file_id == File.id)

            for fp, count in file_paths:
                results[fp] = count
        return results

    @exc_to_thrift_reqfail
    @timeit
    def getRunHistoryTagCounts(self, run_ids, report_filter, cmp_data, limit,
                               offset):
        """
          If the run id list is empty the metrics will be counted
          for all of the runs and in compare mode all of the runs
          will be used as a baseline excluding the runs in compare data.
        """
        self.__require_access()

        limit = verify_limit_range(limit)

        results = []
        with DBSession(self.__Session) as session:
            filter_expression = process_report_filter(session, run_ids,
                                                      report_filter, cmp_data)

            tag_run_ids = session.query(RunHistory.run_id.distinct())

            if run_ids:
                tag_run_ids = tag_run_ids.filter(
                    RunHistory.run_id.in_(run_ids))

            tag_run_ids = tag_run_ids.subquery()

            report_cnt_q = session.query(Report.run_id,
                                         Report.bug_id,
                                         Report.detected_at,
                                         Report.fixed_at) \
                .outerjoin(File, Report.file_id == File.id) \
                .outerjoin(ReviewStatus,
                           ReviewStatus.bug_hash == Report.bug_id) \
                .filter(filter_expression) \
                .filter(Report.run_id.in_(tag_run_ids)) \
                .subquery()

            is_unique = report_filter is not None and report_filter.isUnique
            count_expr = func.count(report_cnt_q.c.bug_id if not is_unique
                                    else report_cnt_q.c.bug_id.distinct())

            count_q = session.query(RunHistory.id.label('run_history_id'),
                                    count_expr.label('report_count')) \
                .outerjoin(report_cnt_q,
                           report_cnt_q.c.run_id == RunHistory.run_id) \
                .filter(get_open_reports_date_filter_query(report_cnt_q.c)) \
                .group_by(RunHistory.id) \
                .subquery()

            tag_q = session.query(RunHistory.run_id.label('run_id'),
                                  RunHistory.id.label('run_history_id'))

            if run_ids:
                tag_q = tag_q.filter(RunHistory.run_id.in_(run_ids))

            if report_filter and report_filter.runTag is not None:
                tag_q = tag_q.filter(RunHistory.id.in_(report_filter.runTag))

            tag_q = tag_q.subquery()

            q = session.query(tag_q.c.run_history_id,
                              func.max(Run.id),
                              func.max(Run.name).label('run_name'),
                              func.max(RunHistory.id),
                              func.max(RunHistory.time),
                              func.max(RunHistory.version_tag),
                              func.max(count_q.c.report_count)) \
                .outerjoin(RunHistory,
                           RunHistory.id == tag_q.c.run_history_id) \
                .outerjoin(Run, Run.id == tag_q.c.run_id) \
                .outerjoin(count_q,
                           count_q.c.run_history_id == RunHistory.id) \
                .group_by(tag_q.c.run_history_id, RunHistory.time) \
                .order_by(RunHistory.time.desc())

            if limit:
                q = q.limit(limit).offset(offset)

            for _, run_id, run_name, tag_id, version_time, tag, count in q:
                results.append(RunTagCount(id=tag_id,
                                           time=str(version_time),
                                           name=tag,
                                           runName=run_name,
                                           runId=run_id,
                                           count=count if count else 0))
        return results

    @exc_to_thrift_reqfail
    @timeit
    def getDetectionStatusCounts(self, run_ids, report_filter, cmp_data):
        """
          If the run id list is empty the metrics will be counted
          for all of the runs and in compare mode all of the runs
          will be used as a baseline excluding the runs in compare data.
        """
        self.__require_access()
        results = {}
        with DBSession(self.__Session) as session:
            filter_expression = process_report_filter(session, run_ids,
                                                      report_filter, cmp_data)

            count_expr = func.count(literal_column('*'))

            q = session.query(Report.detection_status,
                              count_expr)

            q = apply_report_filter(q, filter_expression)

            detection_stats = q.group_by(Report.detection_status).all()

            results = dict(detection_stats)
            results = {
                detection_status_enum(k): v for k,
                v in results.items()}

        return results

    @exc_to_thrift_reqfail
    @timeit
    def getFailedFilesCount(self, run_ids):
        """
        Count the number of uniqued failed files in the latest storage of each
        given run. If the run id list is empty the number of failed files will
        be counted for all of the runs.
        """
        # Unfortunately we can't distinct the failed file paths by using SQL
        # queries because the list of failed files for a run / analyzer are
        # stored in one column in a compressed way. For this reason we need to
        # decompress the value in the Python code before uniqueing.
        return len(self.getFailedFiles(run_ids).keys())

    @exc_to_thrift_reqfail
    @timeit
    def getFailedFiles(self, run_ids):
        """
        Get files which failed to analyze in the latest storage of the given
        runs. For each files it will return a list where each element contains
        information in which run the failure happened.
        """
        self.__require_access()

        res = defaultdict(list)
        with DBSession(self.__Session) as session:
            query, sub_q = get_failed_files_query(
                session, run_ids, [AnalyzerStatistic.failed_files, Run.name],
                [RunHistory.run_id])

            query = query \
                .outerjoin(Run, Run.id == sub_q.c.run_id) \
                .filter(AnalyzerStatistic.failed_files.isnot(None))

            for failed_files, run_name in query.all():
                failed_files = zlib.decompress(failed_files).decode('utf-8')

                for failed_file in failed_files.split('\n'):
                    already_exists = \
                        any(i.runName == run_name for i in res[failed_file])

                    if not already_exists:
                        res[failed_file].append(
                            ttypes.AnalysisFailureInfo(runName=run_name))

        return res

    # -----------------------------------------------------------------------
    @timeit
    def getPackageVersion(self):
        return self.__package_version

    # -----------------------------------------------------------------------
    @exc_to_thrift_reqfail
    @timeit
    def removeRunResults(self, run_ids):
        self.__require_store()

        failed = False
        for run_id in run_ids:
            try:
                self.removeRun(run_id, None)
            except Exception as ex:
                LOG.error("Failed to remove run: %s", run_id)
                LOG.error(ex)
                failed = True
        return not failed

    def __removeReports(self, session, report_ids, chunk_size=500):
        """
        Removing reports in chunks.
        """
        for r_ids in util.chunks(iter(report_ids), chunk_size):
            session.query(Report) \
                .filter(Report.id.in_(r_ids)) \
                .delete(synchronize_session=False)

    @exc_to_thrift_reqfail
    @timeit
    def removeRunReports(self, run_ids, report_filter, cmp_data):
        self.__require_store()

        if not run_ids:
            run_ids = []

        if cmp_data and cmp_data.runIds:
            run_ids.extend(cmp_data.runIds)

        with DBSession(self.__Session) as session:
            check_remove_runs_lock(session, run_ids)

            try:
                filter_expression = process_report_filter(session, run_ids,
                                                          report_filter,
                                                          cmp_data)

                q = session.query(Report.id) \
                    .outerjoin(File, Report.file_id == File.id) \
                    .outerjoin(ReviewStatus,
                               ReviewStatus.bug_hash == Report.bug_id) \
                    .filter(filter_expression)

                reports_to_delete = [r[0] for r in q]
                if reports_to_delete:
                    self.__removeReports(session, reports_to_delete)

                session.commit()
                session.close()

                LOG.info("The following reports were removed by '%s': %s",
                         self.__get_username(), reports_to_delete)
            except Exception as ex:
                session.rollback()
                LOG.error("Database cleanup failed.")
                LOG.error(ex)
                return False

        # Remove unused data (files, comments, etc.) from the database.
        db_cleanup.remove_unused_data(self.__Session)

        return True

    @exc_to_thrift_reqfail
    @timeit
    def removeRun(self, run_id, run_filter):
        self.__require_store()

        # Remove the whole run.
        with DBSession(self.__Session) as session:
            check_remove_runs_lock(session, [run_id])

            if not run_filter:
                run_filter = RunFilter(ids=[run_id])

            q = session.query(Run)
            q = process_run_filter(session, q, run_filter)
            q.delete(synchronize_session=False)

            session.commit()
            session.close()

            runs = run_filter.names if run_filter.names else run_filter.ids
            LOG.info("Runs '%s' were removed by '%s'.", runs,
                     self.__get_username())

        # Remove unused data (files, comments, etc.) from the database.
        db_cleanup.remove_unused_data(self.__Session)

        return True

    @exc_to_thrift_reqfail
    @timeit
    def updateRunData(self, run_id, new_run_name):
        self.__require_store()

        if not new_run_name:
            msg = 'No new run name was given to update the run.'
            LOG.error(msg)
            raise codechecker_api_shared.ttypes.RequestFailed(
                codechecker_api_shared.ttypes.ErrorCode.GENERAL, msg)

        with DBSession(self.__Session) as session:
            check_new_run_name = session.query(Run) \
                .filter(Run.name == new_run_name) \
                .all()
            if check_new_run_name:
                msg = "New run name '" + new_run_name + "' already exists."
                LOG.error(msg)

                raise codechecker_api_shared.ttypes.RequestFailed(
                    codechecker_api_shared.ttypes.ErrorCode.DATABASE, msg)

            run_data = session.query(Run).get(run_id)
            if run_data:
                old_run_name = run_data.name
                run_data.name = new_run_name
                session.add(run_data)
                session.commit()

                LOG.info("Run name '%s' (%d) was changed to %s by '%s'.",
                         old_run_name, run_id, new_run_name,
                         self.__get_username())

                return True
            else:
                msg = 'Run id ' + str(run_id) + \
                      ' was not found in the database.'
                LOG.error(msg)
                raise codechecker_api_shared.ttypes.RequestFailed(
                    codechecker_api_shared.ttypes.ErrorCode.DATABASE, msg)

        return True

    @exc_to_thrift_reqfail
    def getSuppressFile(self):
        """
        DEPRECATED the server is not started with a suppress file anymore.
        Returning empty string.
        """
        self.__require_access()
        return ''

    @exc_to_thrift_reqfail
    @timeit
    def addSourceComponent(self, name, value, description):
        """
        Adds a new source if it does not exist or updates an old one.
        """
        self.__require_admin()
        with DBSession(self.__Session) as session:
            component = session.query(SourceComponent).get(name)
            user = self.__auth_session.user if self.__auth_session else None

            if component:
                component.value = value.encode('utf-8')
                component.description = description
                component.user = user
            else:
                component = SourceComponent(name,
                                            value.encode('utf-8'),
                                            description,
                                            user)

            session.add(component)
            session.commit()

            return True

    @exc_to_thrift_reqfail
    @timeit
    def getSourceComponents(self, component_filter):
        """
        Returns the available source components.
        """
        self.__require_access()
        with DBSession(self.__Session) as session:
            q = session.query(SourceComponent)

            if component_filter:
                sql_component_filter = [SourceComponent.name.ilike(conv(cf))
                                        for cf in component_filter]
                q = q.filter(*sql_component_filter)

            q = q.order_by(SourceComponent.name)

            components = [SourceComponentData(c.name, c.value.decode('utf-8'),
                                              c.description) for c in q]

            # If no filter is set or the auto generated component name can
            # be found in the filter list we will return with this
            # component too.
            if not component_filter or \
                    GEN_OTHER_COMPONENT_NAME in component_filter:
                component_other = \
                    SourceComponentData(GEN_OTHER_COMPONENT_NAME, None,
                                        "Special auto-generated source "
                                        "component which contains files that "
                                        "are uncovered by the rest of the "
                                        "components.")

                components.append(component_other)

            return components

    @exc_to_thrift_reqfail
    @timeit
    def removeSourceComponent(self, name):
        """
        Removes a source component.
        """
        self.__require_admin()

        with DBSession(self.__Session) as session:
            component = session.query(SourceComponent).get(name)
            if component:
                session.delete(component)
                session.commit()
                LOG.info("Source component '%s' has been removed by '%s'",
                         name, self.__get_username())
                return True
            else:
                msg = 'Source component ' + str(name) + \
                      ' was not found in the database.'
                raise codechecker_api_shared.ttypes.RequestFailed(
                    codechecker_api_shared.ttypes.ErrorCode.DATABASE, msg)

    @exc_to_thrift_reqfail
    @timeit
    def getMissingContentHashes(self, file_hashes):
        self.__require_store()

        if not file_hashes:
            return []

        with DBSession(self.__Session) as session:

            q = session.query(FileContent) \
                .options(sqlalchemy.orm.load_only('content_hash')) \
                .filter(FileContent.content_hash.in_(file_hashes))

            return list(set(file_hashes) -
                        set([fc.content_hash for fc in q]))

    def __store_source_files(self, source_root, filename_to_hash,
                             trim_path_prefixes):
        """
        Storing file contents from plist.
        """

        file_path_to_id = {}

        for file_name, file_hash in filename_to_hash.items():
            source_file_name = os.path.join(source_root,
                                            file_name.strip("/"))
            source_file_name = os.path.realpath(source_file_name)
            LOG.debug("Storing source file: %s", source_file_name)
            trimmed_file_path = util.trim_path_prefixes(file_name,
                                                        trim_path_prefixes)

            if not os.path.isfile(source_file_name):
                # The file was not in the ZIP file, because we already
                # have the content. Let's check if we already have a file
                # record in the database or we need to add one.

                LOG.debug('%s not found or already stored.', trimmed_file_path)
                with DBSession(self.__Session) as session:
                    fid = store_handler.addFileRecord(session,
                                                      trimmed_file_path,
                                                      file_hash)
                if not fid:
                    LOG.error("File ID for %s is not found in the DB with "
                              "content hash %s. Missing from ZIP?",
                              source_file_name, file_hash)
                file_path_to_id[trimmed_file_path] = fid
                LOG.debug("%d fileid found", fid)
                continue

            with DBSession(self.__Session) as session:
                file_path_to_id[trimmed_file_path] = \
                    store_handler.addFileContent(session,
                                                 trimmed_file_path,
                                                 source_file_name,
                                                 file_hash,
                                                 None)

        return file_path_to_id

    def __process_report_file(
        self,
        report_file_path: str,
        session: DBSession,
        source_root: str,
        run_id: int,
        file_path_to_id: Dict[str, int],
        run_history_time: datetime,
        severity_map: webserver_context.SeverityMap,
        wrong_src_code_comments: List[str],
        skip_handler: Optional[skiplist_handler.SkipListHandler],
        trim_path_prefixes: List[str],
        mip: MetadataInfoParser,
        already_added_report_hashes: Set[str],
        new_report_hashes: Set[str],
        hash_map_reports: Dict[str, List[Any]],
        all_report_checkers: Set[str]
    ) -> bool:
        """
        Process and save reports from the given report file to the database.
        """
        try:
            files, reports = plist_parser.parse_plist_file(report_file_path)
        except Exception as ex:
            LOG.warning('Parsing the plist failed: %s', str(ex))
            return False

        if not reports:
            return True

        trimmed_files = {}
        file_ids = {}
        missing_ids_for_files = []

        for k, v in files.items():
            trimmed_files[k] = \
                util.trim_path_prefixes(v, trim_path_prefixes)

        for file_name in trimmed_files.values():
            file_id = file_path_to_id.get(file_name, -1)
            if file_id == -1:
                missing_ids_for_files.append(file_name)
                continue

            file_ids[file_name] = file_id

        if missing_ids_for_files:
            LOG.warning("Failed to get file path id for '%s'!",
                        ' '.join(missing_ids_for_files))
            return False

        def set_review_status(report: Any):
            """
            Set review status for the given report if there is any source code
            comment.
            """
            checker_name = report.main['check_name']
            last_report_event = report.bug_path[-1]

            # The original file path is needed here not the trimmed
            # because the source files are extracted as the original
            # file path.
            file_name = files[last_report_event['location']['file']]

            source_file_name = os.path.realpath(
                os.path.join(source_root, file_name.strip("/")))

            # Check and store source code comments.
            if not os.path.isfile(source_file_name):
                return

            report_line = last_report_event['location']['line']
            source_file = os.path.basename(file_name)

            src_comment_data = parse_codechecker_review_comment(
                source_file_name, report_line, checker_name)

            if len(src_comment_data) == 1:
                status = src_comment_data[0]['status']
                rw_status = ttypes.ReviewStatus.FALSE_POSITIVE
                if status == 'confirmed':
                    rw_status = ttypes.ReviewStatus.CONFIRMED
                elif status == 'intentional':
                    rw_status = ttypes.ReviewStatus.INTENTIONAL

                self._setReviewStatus(
                    session, report.report_hash, rw_status,
                    src_comment_data[0]['message'], run_history_time)
            elif len(src_comment_data) > 1:
                LOG.warning(
                    "Multiple source code comment can be found "
                    "for '%s' checker in '%s' at line %s. "
                    "This bug will not be suppressed!",
                    checker_name, source_file, report_line)

                wrong_src_code_comments.append(
                    f"{source_file}|{report_line}|{checker_name}")

        for report in reports:
            all_report_checkers.add(report.check_name)

            if skip_handler and skip_handler.should_skip(report.file_path):
                continue

            report.trim_path_prefixes(trim_path_prefixes)

            bug_paths, bug_events, bug_extended_data = \
                store_handler.collect_paths_events(report, file_ids,
                                                   trimmed_files)
            report_path_hash = get_report_path_hash(report)
            if report_path_hash in already_added_report_hashes:
                LOG.debug('Not storing report. Already added: %s', report)
                continue

            LOG.debug("Storing report to the database...")

            bug_id = report.report_hash

            detection_status = 'new'
            detected_at = run_history_time

            if bug_id in hash_map_reports:
                old_report = hash_map_reports[bug_id][0]
                old_status = old_report.detection_status
                detection_status = 'reopened' \
                    if old_status == 'resolved' else 'unresolved'
                detected_at = old_report.detected_at

            analyzer_name = get_analyzer_name(
                report.check_name, mip.checker_to_analyzer, report.metadata)

            report_id = store_handler.addReport(
                session, run_id, file_ids[report.file_path], report.main,
                bug_paths, bug_events, bug_extended_data, detection_status,
                detected_at, severity_map, analyzer_name)

            new_report_hashes.add(bug_id)
            already_added_report_hashes.add(report_path_hash)

            set_review_status(report)

            LOG.debug("Storing report done. ID=%d", report_id)

        return True

    def __store_reports(self, session, report_dir, source_root, run_id,
                        file_path_to_id, run_history_time, severity_map,
                        wrong_src_code_comments, trim_path_prefixes):
        """
        Parse up and store the plist report files.
        """
        def get_skip_handler(
            report_dir: str
        ) -> Optional[skiplist_handler.SkipListHandler]:
            """ Get a skip list handler based on the given report directory."""
            skip_file_path = os.path.join(report_dir, 'skip_file')
            if not os.path.exists(skip_file_path):
                return

            LOG.debug("Pocessing skip file %s", skip_file_path)
            try:
                with open(skip_file_path,
                          encoding="utf-8", errors="ignore") as f:
                    skip_content = f.read()
                    LOG.debug(skip_content)

                    return skiplist_handler.SkipListHandler(skip_content)
            except (IOError, OSError) as err:
                LOG.warning("Failed to open skip file: %s", err)

        all_reports = session.query(Report) \
            .filter(Report.run_id == run_id) \
            .all()

        hash_map_reports = defaultdict(list)
        for report in all_reports:
            hash_map_reports[report.bug_id].append(report)

        already_added_report_hashes = set()
        new_report_hashes = set()
        enabled_checkers = set()
        disabled_checkers = set()
        all_report_checkers = set()

        # Processing PList files.
        for root_dir_path, _, report_file_paths in os.walk(report_dir):
            LOG.debug("Get reports from '%s' directory", root_dir_path)

            skip_handler = get_skip_handler(root_dir_path)

            metadata_file_path = os.path.join(root_dir_path, 'metadata.json')
            mip = MetadataInfoParser(metadata_file_path)

            enabled_checkers.update(mip.enabled_checkers)
            disabled_checkers.update(mip.disabled_checkers)

            for f in report_file_paths:
                if not f.endswith('.plist'):
                    continue

                LOG.debug("Parsing input file '%s'", f)

                report_file_path = os.path.join(root_dir_path, f)
                self.__process_report_file(
                    report_file_path, session, source_root, run_id,
                    file_path_to_id, run_history_time, severity_map,
                    wrong_src_code_comments, skip_handler, trim_path_prefixes,
                    mip, already_added_report_hashes, new_report_hashes,
                    hash_map_reports, all_report_checkers)

        # If a checker was found in a plist file it can not be disabled so we
        # will add this to the enabled checkers list and remove this checker
        # from the disabled checkers list.
        # Also if multiple report directories are stored and a checker was
        # enabled in one report directory but it was disabled in another
        # directory we will mark this checker as enabled.
        enabled_checkers |= all_report_checkers
        disabled_checkers -= all_report_checkers

        reports_to_delete = set()
        for bug_hash, reports in hash_map_reports.items():
            if bug_hash in new_report_hashes:
                reports_to_delete.update([x.id for x in reports])
            else:
                for report in reports:
                    # We set the fix date of a report only if the report
                    # has not been fixed before.
                    if report.fixed_at:
                        continue

                    checker = report.checker_id
                    if checker in disabled_checkers:
                        report.detection_status = 'off'
                    elif checker_is_unavailable(checker, enabled_checkers):
                        report.detection_status = 'unavailable'
                    else:
                        report.detection_status = 'resolved'

                    report.fixed_at = run_history_time

        if reports_to_delete:
            self.__removeReports(session, list(reports_to_delete))

    @staticmethod
    @exc_to_thrift_reqfail
    def __store_run_lock(session, name, username):
        """
        Store a RunLock record for the given run name into the database.
        """
        try:
            # If the run can be stored, we need to lock it first. If there is
            # already a lock in the database for the given run name which is
            # expired and multiple processes are trying to get this entry from
            # the database for update we may get the following exception:
            # could not obtain lock on row in relation "run_locks"
            # This is the reason why we have to wrap this query to a try/except
            # block.
            run_lock = session.query(RunLock) \
                .filter(RunLock.name == name) \
                .with_for_update(nowait=True).one_or_none()
        except (sqlalchemy.exc.OperationalError,
                sqlalchemy.exc.ProgrammingError) as ex:
            LOG.error("Failed to get run lock for '%s': %s", name, ex)
            raise codechecker_api_shared.ttypes.RequestFailed(
                codechecker_api_shared.ttypes.ErrorCode.DATABASE,
                "Someone is already storing to the same run. Please wait "
                "while the other storage is finished and try it again.")

        if not run_lock:
            # If there is no lock record for the given run name, the run
            # is not locked -- create a new lock.
            run_lock = RunLock(name, username)
            session.add(run_lock)
        elif run_lock.has_expired(
                db_cleanup.RUN_LOCK_TIMEOUT_IN_DATABASE):
            # There can be a lock in the database, which has already
            # expired. In this case, we assume that the previous operation
            # has failed, and thus, we can re-use the already present lock.
            run_lock.touch()
            run_lock.username = username
        else:
            # In case the lock exists and it has not expired, we must
            # consider the run a locked one.
            when = run_lock.when_expires(
                db_cleanup.RUN_LOCK_TIMEOUT_IN_DATABASE)

            username = run_lock.username if run_lock.username is not None \
                else "another user"

            LOG.info("Refusing to store into run '%s' as it is locked by "
                     "%s. Lock will expire at '%s'.", name, username, when)
            raise codechecker_api_shared.ttypes.RequestFailed(
                codechecker_api_shared.ttypes.ErrorCode.DATABASE,
                "The run named '{0}' is being stored into by {1}. If the "
                "other store operation has failed, this lock will expire "
                "at '{2}'.".format(name, username, when))

        # At any rate, if the lock has been created or updated, commit it
        # into the database.
        try:
            session.commit()
        except (sqlalchemy.exc.IntegrityError,
                sqlalchemy.orm.exc.StaleDataError):
            # The commit of this lock can fail.
            #
            # In case two store ops attempt to lock the same run name at the
            # same time, committing the lock in the transaction that commits
            # later will result in an IntegrityError due to the primary key
            # constraint.
            #
            # In case two store ops attempt to lock the same run name with
            # reuse and one of the operation hangs long enough before COMMIT
            # so that the other operation commits and thus removes the lock
            # record, StaleDataError is raised. In this case, also consider
            # the run locked, as the data changed while the transaction was
            # waiting, as another run wholly completed.

            LOG.info("Run '%s' got locked while current transaction "
                     "tried to acquire a lock. Considering run as locked.",
                     name)
            raise codechecker_api_shared.ttypes.RequestFailed(
                codechecker_api_shared.ttypes.ErrorCode.DATABASE,
                "The run named '{0}' is being stored into by another "
                "user.".format(name))

    @staticmethod
    @exc_to_thrift_reqfail
    def __free_run_lock(session, name):
        """
        Remove the lock from the database for the given run name.
        """
        # Using with_for_update() here so the database (in case it supports
        # this operation) locks the lock record's row from any other access.
        run_lock = session.query(RunLock) \
            .filter(RunLock.name == name) \
            .with_for_update(nowait=True).one()
        session.delete(run_lock)
        session.commit()

    def __check_run_limit(self, run_name):
        """
        Checks the maximum allowed of uploadable runs for the current product.
        """
        max_run_count = self.__manager.get_max_run_count()

        with DBSession(self.__config_database) as session:
            product = session.query(Product).get(self.__product.id)
            if product.run_limit:
                max_run_count = product.run_limit

        # Session that handles constraints on the run.
        with DBSession(self.__Session) as session:
            if max_run_count:
                LOG.debug("Check the maximum number of allowed "
                          "runs which is %d", max_run_count)

                run = session.query(Run) \
                    .filter(Run.name == run_name) \
                    .one_or_none()

                # If max_run_count is not set in the config file, it will allow
                # the user to upload unlimited runs.

                run_count = session.query(Run.id).count()

                # If we are not updating a run or the run count is reached the
                # limit it will throw an exception.
                if not run and run_count >= max_run_count:
                    remove_run_count = run_count - max_run_count + 1
                    raise codechecker_api_shared.ttypes.RequestFailed(
                        codechecker_api_shared.ttypes.ErrorCode.GENERAL,
                        'You reached the maximum number of allowed runs '
                        '({0}/{1})! Please remove at least {2} run(s) before '
                        'you try it again.'.format(run_count,
                                                   max_run_count,
                                                   remove_run_count))

    @exc_to_thrift_reqfail
    @timeit
    def massStoreRun(self, name, tag, version, b64zip, force,
                     trim_path_prefixes, description):
        self.__require_store()
        start_time = time.time()

        user = self.__auth_session.user if self.__auth_session else None

        # Check constraints of the run.
        self.__check_run_limit(name)

        with DBSession(self.__Session) as session:
            ThriftRequestHandler.__store_run_lock(session, name, user)

        wrong_src_code_comments = []
        try:
            with TemporaryDirectory() as zip_dir:
                LOG.info("[%s] Unzip storage file...", name)
                zip_size = unzip(b64zip, zip_dir)
                LOG.info("[%s] Unzip storage file done.", name)

                if zip_size == 0:
                    raise codechecker_api_shared.ttypes.RequestFailed(
                        codechecker_api_shared.ttypes.
                        ErrorCode.GENERAL,
                        "The received zip file content is empty"
                        " nothing was stored.")

                LOG.debug("Using unzipped folder '%s'", zip_dir)

                source_root = os.path.join(zip_dir, 'root')
                report_dir = os.path.join(zip_dir, 'reports')
                metadata_file = os.path.join(report_dir, 'metadata.json')
                content_hash_file = os.path.join(zip_dir,
                                                 'content_hashes.json')

                filename_to_hash = \
                    util.load_json_or_empty(content_hash_file, {})

                LOG.info("[%s] Store source files...", name)
                file_path_to_id = self.__store_source_files(source_root,
                                                            filename_to_hash,
                                                            trim_path_prefixes)
                LOG.info("[%s] Store source files done.", name)

                run_history_time = datetime.now()

                mip = MetadataInfoParser(metadata_file)

                command = ''
                if len(mip.check_commands) == 1:
                    command = list(mip.check_commands)[0]
                elif len(mip.check_commands) > 1:
                    command = "multiple analyze calls: " + \
                              '; '.join(mip.check_commands)

                durations = 0
                if mip.check_durations:
                    # Round the duration to seconds.
                    durations = int(sum(mip.check_durations))

                # When we use multiple server instances and we try to run
                # multiple storage to each server which contain at least two
                # reports which have the same report hash and have source code
                # comments it is possible that the following exception will be
                # thrown: (psycopg2.extensions.TransactionRollbackError)
                # deadlock detected.
                # The problem is that the report hash is the key for the
                # review data table and both of the store actions try to
                # update the same review data row.
                # Neither of the two processes can continue, and they will wait
                # for each other indefinitely. PostgreSQL in this case will
                # terminate one transaction with the above exception.
                # For this reason in case of failure we will wait some seconds
                # and try to run the storage again.
                # For more information see #2655 and #2653 issues on github.
                max_num_of_tries = 3
                num_of_tries = 0
                sec_to_wait_after_failure = 60
                while True:
                    try:
                        # This session's transaction buffer stores the actual
                        # run data into the database.
                        with DBSession(self.__Session) as session:
                            # Load the lock record for "FOR UPDATE" so that the
                            # transaction that handles the run's store
                            # operations has a lock on the database row itself.
                            run_lock = session.query(RunLock) \
                                .filter(RunLock.name == name) \
                                .with_for_update(nowait=True).one()

                            # Do not remove this seemingly dummy print, we need
                            # to make sure that the execution of the SQL
                            # statement is not optimised away and the fetched
                            # row is not garbage collected.
                            LOG.debug("Storing into run '%s' locked at '%s'.",
                                      name, run_lock.locked_at)

                            # Actual store operation begins here.
                            user_name = self.__get_username()
                            run_id = store_handler.addCheckerRun(
                                session, command, name, tag, user_name,
                                run_history_time, version, force,
                                mip.cc_version, mip.analyzer_statistics,
                                description)

                            LOG.info("[%s] Store reports...", name)
                            self.__store_reports(
                                session, report_dir, source_root, run_id,
                                file_path_to_id, run_history_time,
                                self.__context.severity_map,
                                wrong_src_code_comments, trim_path_prefixes)
                            LOG.info("[%s] Store reports done.", name)

                            store_handler.setRunDuration(session,
                                                         run_id,
                                                         durations)

                            store_handler.finishCheckerRun(session, run_id)

                            session.commit()

                            LOG.info("'%s' stored results (%s KB "
                                     "/decompressed/) to run '%s' in %s "
                                     "seconds.", user_name,
                                     round(zip_size / 1024), name,
                                     round(time.time() - start_time, 2))

                            return run_id
                    except (sqlalchemy.exc.OperationalError,
                            sqlalchemy.exc.ProgrammingError) as ex:
                        num_of_tries += 1

                        if num_of_tries == max_num_of_tries:
                            raise codechecker_api_shared.ttypes.RequestFailed(
                                codechecker_api_shared.ttypes.
                                ErrorCode.DATABASE,
                                "Storing reports to the database failed: "
                                "{0}".format(ex))

                        LOG.error("Storing reports of '%s' run failed: "
                                  "%s.\nWaiting %d sec before trying to store "
                                  "it again!", name, ex,
                                  sec_to_wait_after_failure)
                        time.sleep(sec_to_wait_after_failure)
                        sec_to_wait_after_failure *= 2
        except Exception as ex:
            LOG.error("Failed to store results: %s", ex)
            import traceback
            traceback.print_exc()
            raise
        finally:
            # In any case if the "try" block's execution began, a run lock must
            # exist, which can now be removed, as storage either completed
            # successfully, or failed in a detectable manner.
            # (If the failure is undetectable, the coded grace period expiry
            # of the lock will allow further store operations to the given
            # run name.)
            with DBSession(self.__Session) as session:
                ThriftRequestHandler.__free_run_lock(session, name)

            if wrong_src_code_comments:
                raise codechecker_api_shared.ttypes.RequestFailed(
                    codechecker_api_shared.ttypes.ErrorCode.SOURCE_FILE,
                    "Multiple source code comment can be found with the same "
                    "checker name for same bug!",
                    wrong_src_code_comments)

    @exc_to_thrift_reqfail
    @timeit
    def allowsStoringAnalysisStatistics(self):
        self.__require_store()

        return True if self.__manager.get_analysis_statistics_dir() else False

    @exc_to_thrift_reqfail
    @timeit
    def getAnalysisStatisticsLimits(self):
        self.__require_store()

        cfg = dict()

        # Get the limit of failure zip size.
        failure_zip_size = self.__manager.get_failure_zip_size()
        if failure_zip_size:
            cfg[ttypes.StoreLimitKind.FAILURE_ZIP_SIZE] = failure_zip_size

        # Get the limit of compilation database size.
        compilation_database_size = \
            self.__manager.get_compilation_database_size()
        if compilation_database_size:
            cfg[ttypes.StoreLimitKind.COMPILATION_DATABASE_SIZE] = \
                compilation_database_size

        return cfg

    @exc_to_thrift_reqfail
    @timeit
    def storeAnalysisStatistics(self, run_name, b64zip):
        self.__require_store()

        report_dir_store = self.__manager.get_analysis_statistics_dir()
        if report_dir_store:
            try:
                product_dir = os.path.join(report_dir_store,
                                           self.__product.endpoint)
                # Create report store directory.
                if not os.path.exists(product_dir):
                    os.makedirs(product_dir)

                # Removes and replaces special characters in the run name.
                run_name = slugify(run_name)
                run_zip_file = os.path.join(product_dir, run_name + '.zip')
                with open(run_zip_file, 'wb') as run_zip:
                    run_zip.write(zlib.decompress(
                        base64.b64decode(b64zip.encode('utf-8'))))
                return True
            except Exception as ex:
                LOG.error(str(ex))
                return False

        return False

    @exc_to_thrift_reqfail
    @timeit
    def getAnalysisStatistics(self, run_id, run_history_id):
        self.__require_access()

        analyzer_statistics = {}

        with DBSession(self.__Session) as session:
            run_ids = None if run_id is None else [run_id]
            run_history_ids = None if run_history_id is None \
                else [run_history_id]

            query = get_analysis_statistics_query(
                session, run_ids, run_history_ids)

            for stat, run_id in query:
                failed_files = zlib.decompress(stat.failed_files).decode(
                    'utf-8').split('\n') if stat.failed_files else None
                analyzer_version = zlib.decompress(
                    stat.version).decode('utf-8') if stat.version else None

                analyzer_statistics[stat.analyzer_type] = \
                    ttypes.AnalyzerStatistics(version=analyzer_version,
                                              failed=stat.failed,
                                              failedFilePaths=failed_files,
                                              successful=stat.successful)
        return analyzer_statistics

    @exc_to_thrift_reqfail
    @timeit
    def exportData(self, run_filter):
        self.__require_access()

        with DBSession(self.__Session) as session:

            # Logic for getting comments
            comment_data_list = defaultdict(list)
            comment_query = session.query(Comment, Report.bug_id) \
                .outerjoin(Report, Report.bug_id == Comment.bug_hash) \
                .order_by(Comment.created_at.desc())

            if run_filter:
                comment_query = process_run_filter(session, comment_query,
                                                   run_filter) \
                    .outerjoin(Run, Report.run_id == Run.id)

            for data, report_id in comment_query:
                comment_data = ttypes.CommentData(
                    id=data.id,
                    author=data.author,
                    message=data.message.decode('utf-8'),
                    createdAt=str(data.created_at),
                    kind=data.kind)
                comment_data_list[report_id].append(comment_data)

            # Logic for getting review status
            review_data_list = {}
            review_query = session.query(ReviewStatus, Report.bug_id) \
                .outerjoin(Report, Report.bug_id == ReviewStatus.bug_hash) \
                .order_by(ReviewStatus.date.desc())

            if run_filter:
                review_query = process_run_filter(session, review_query,
                                                  run_filter) \
                    .outerjoin(Run, Report.run_id == Run.id)

            for data, report_id in review_query:
                review_data = create_review_data(data)
                review_data_list[report_id] = review_data

        return ExportData(comments=comment_data_list,
                          reviewData=review_data_list)

    @exc_to_thrift_reqfail
    @timeit
    def importData(self, exportData):
        self.__require_admin()
        with DBSession(self.__Session) as session:

            # Logic for importing comments
            comment_bug_ids = list(exportData.comments.keys())
            comment_query = session.query(Comment) \
                .filter(Comment.bug_hash.in_(comment_bug_ids)) \
                .order_by(Comment.created_at.desc())
            comments_in_db = defaultdict(list)
            for comment in comment_query:
                comments_in_db[comment.bug_hash].append(comment)
            for bug_hash, comments in exportData.comments.items():
                db_comments = comments_in_db[bug_hash]
                for comment in comments:
                    date = datetime.strptime(comment.createdAt,
                                             '%Y-%m-%d %H:%M:%S.%f')
                    message = comment.message.encode('utf-8') \
                        if comment.message else b''
                    # See if the comment is already in the database.
                    if any(c.created_at == date and
                           c.kind == comment.kind and
                           c.message == message for c in db_comments):
                        continue
                    c = Comment(bug_hash, comment.author, message,
                                comment.kind, date)
                    session.add(c)

            # Logic for importing review status
            review_bug_ids = list(exportData.reviewData.keys())
            review_query = session.query(ReviewStatus) \
                .filter(ReviewStatus.bug_hash.in_(review_bug_ids)) \
                .order_by(ReviewStatus.date.desc())
            db_review_data = {}
            for review_status in review_query:
                db_review_data[review_status.bug_hash] = review_status
            for bug_hash, imported_review in exportData.reviewData.items():
                db_status = db_review_data.get(bug_hash)
                # The status is up-to-date.
                if db_status and str(db_status.date) == imported_review.date:
                    continue
                date = datetime.strptime(imported_review.date,
                                         '%Y-%m-%d %H:%M:%S.%f')
                self._setReviewStatus(session,
                                      bug_hash,
                                      imported_review.status,
                                      imported_review.comment,
                                      date)

            session.commit()
            return True
