import json
import sys
import warnings
from copy import deepcopy
from io import BytesIO

import pandas as pd
import numpy as np
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound
from common.models import Data, Job, Case
from common.util import ClickhouseClient, escape_string, log_time_decorator
from job_page.layout import render_layout
from job_list.views import LAYOUT_TEMPLATE as META_LAYOUT_TEMPLATE
from urllib.parse import urlparse


if not sys.warnoptions:
    warnings.simplefilter("ignore")

LABEL_MAP = {
    '^Memory$': 'bytes',
    '^cpu-cpu': '%',
    '^current$': 'mA',
}


def _retrive_base_url(request):
    full_url = request.build_absolute_uri()
    parsed_url = urlparse(full_url)
    base_url = '{}://{}'.format(parsed_url.scheme, parsed_url.netloc)
    return base_url


@log_time_decorator  # дебажный логгинг.
def get_layout(request):
    """
    Формирует layout для страницы отчета на основе метаинформации и параметров взапросе.
    Довольно жутковата, так как layout периодически перепридумывается,
        и нет четкого представления, как работать с разными срезами данных, группировкой по хостам, группам метрик,
        гильзам или иным признакам

    принимает в запросе:
    - job: номер стрельбы

    :param request: django HTTP request
    :return: django HTTP response
    """
    try:
        job_id = request.GET.get('job')
        assert job_id and job_id.isdigit(), 'invalid job: %s' % {job_id}
        job_id = int(job_id)
        job_obj = Job.objects.get(id=job_id)
    except AssertionError as aexc:
        return HttpResponseBadRequest(json.dumps(repr(aexc)))
    except Job.DoesNotExist:
        return HttpResponseNotFound('Job with id {} not found'.format(job_id))

    meta = {job_obj.id: deepcopy(META_LAYOUT_TEMPLATE)}
    for m in meta[job_obj.id]:
        m['locations'] = []
        if m.get('label') not in ('Status', 'Name'):
            m['locations'].append('info')
        m['isMutable'] = isinstance(m.get('dataSource'), str) and m.get('dataSource') not in job_obj.immutable_meta

    base_url = _retrive_base_url(request)
    layout = render_layout(job_obj, meta, base_url)

    for widget in layout['sections'][0]['widgets']:
        if len(widget.get('views', [])) > 0:
            layout['navigation'].update(layout['sections'][0]['widgets'][0]['views'][0])
            break

    return HttpResponse(
        json.dumps(layout), content_type='application/json; charset=utf-8'
    )


def data_getter(fn):
    def decorated(request):
        data_ids = [escape_string(tag) for tag in request.GET.getlist('tag')]
        try:
            data_objects = Data.objects.filter(id__in=data_ids)
        except ValueError as e:
            return HttpResponseBadRequest('invalid tag: {}'.format(e))
        if not data_objects:
            return HttpResponseBadRequest('invalid tag')
        cases = request.GET.get('cases', Case.OVERALL).split('|')
        case_objs = []
        for do in data_objects:
            if do.meta['name'] in ['planned_rps', 'actual_rps']:
                case_objs += [do.case_set.first()]
            else:
                try:
                    case_objs += [do.case_set.get(name=case) for case in cases]
                except Case.DoesNotExist:
                    pass
        tags = [case.tag.hex for case in case_objs]
        return fn(request, data_objects, case_objs, tags)
    return decorated


@data_getter
def get_metrics(request, data_objects, case_objects, tags):
    """
    Ручка старого образца, она до сих пор используется на странице теста
    Ручки в приложении compare более универсальны и следует использовать именно их и тут и там.
    :param request: django HTTP request
    :return: django HTTP response
    """
    # FIXME: выпилить эту ручку в пользу ручки из приложения compare
    normalize = bool(int(request.GET.get('normalize', 0)))
    if not all([d.type == 'metrics' for d in data_objects]):
        return HttpResponseBadRequest('This handler is for metrics only. For events use /get_events/?tag=...')
    test_start = Job.objects.get(id=data_objects.first().job_id).test_start
    test_start = int(test_start)
    offset = data_objects.first().offset

    try:
        start = request.GET.get('start') or request.GET.get('from') or test_start + offset
        start = int(start)
        end = request.GET.get('end') or request.GET.get('to') or 0
        end = int(end)
    except ValueError:
        return HttpResponseBadRequest('"start", "end", "from" and "to" must be digital')

    ch_client = ClickhouseClient()
    if not end:
        end = ch_client.select("select max(ts) from metrics where tag in ('%s')" % "','".join(tags))
        if end:
            end = int(end[0][0])
            # прибавлять и потом отнимать test_start+offset приходится потому, что с фронта будут приходить таймстемпы,
            # а в базе метрики хранятся от нуля.
            end += test_start + offset
        else:
            return HttpResponse('timestamp', content_type='text/csv')

    try:
        dots = int(request.GET.get('dots', 1000))
        assert dots > 0
    except ValueError:
        return HttpResponseBadRequest('"dots" must be digital')
    except AssertionError:
        return HttpResponseBadRequest('"dots" must be > 0')
    dots_available = _dots_available(tags[0], start, end, test_start, offset)
    if not dots_available:
        return HttpResponse('timestamp', content_type='text/csv')
    compress_ratio = _compress_ratio(start, end, dots, dots_available)

    query, query_params = _sql_for_get_metrics(tags, start, end, test_start, offset, compress_ratio)

    df = pd.read_csv(
        BytesIO(ch_client.select_csv(query, query_params=query_params)),
        names=['tag', 'timestamp', 'value'])
    df = df.set_index(['timestamp', 'tag']).\
        unstack()['value'].\
        rename(columns=lambda t: Case.objects.get(tag=t).data.id)

    # применяем diff к метрикам, у которых есть соответствующая отметка
    data_with_diff = [data.uniq_id for data in data_objects if data.meta.get('_apply', '') == 'diff']
    for tag in df.columns:
        if tag in data_with_diff:
            tag_df = df.loc[:, tag].dropna().diff()
            df.loc[:, tag] = tag_df

    # нормализуем данные в интервал от 0 до 1
    if normalize:
        df = df.apply(lambda x: x / x.max(), axis=0)

    # Заполняем пропуски на таймлайне пустыми значениями
    if df.index.size > 1:
        index_interval = int(pd.Series(df.index).diff().mode()[0])
        new_index = range(df.index[0], df.index[-1], index_interval)
        df = df.reindex(new_index, fill_value='', method='nearest', tolerance=index_interval)

    return HttpResponse(df.to_csv(), content_type='text/csv')


def _sql_for_get_metrics(tags, start, end, test_start, offset, compress_ratio):
    """
    :param tags: тэг метрики (str)
    :param start: начало запрашиваемого интервала (микросекунды int)
    :param end: конец запрашиваемого интервала (микросекунды int)
    :param test_start: время начала теста (микросекунды int)
    :param offset: офсет метрики (микросекунды int)
    :param compress_ratio:
    :return:
    """
    query = '''
            select any(tag), intDiv(toInt64(ts)+{test_start}+{offset}, {compress_ratio})*{compress_ratio} as t, avg(value)
            from metrics 
            where tag in ('{tags}')
            and ts >= toInt64({start}) - {test_start} - {offset}
            and ts <= toInt64({end}) - {test_start} - {offset}
            group by t, tag
            order by t, tag
        '''
    query_params = {
        'tags': "','".join(tags),
        'start': start,
        'end': end,
        'test_start': test_start,
        'offset': offset,
        'compress_ratio': compress_ratio,
    }
    return query, query_params


def no_data_response():
    return HttpResponse('timestamp', content_type='text/csv')


@data_getter
def get_histograms(request, data_objects, cases, tags):
    """
    :param request: django HTTP request
    :return:
    """
    offset = request.GET.get('interval_offset')
    interval = request.GET.get('interval')
    try:
        start = int(offset) if offset else 0
        end = start + int(interval) if interval else None
    except ValueError:
        return HttpResponseBadRequest('"interval_offset", "interval" must be numbers')
    else:
        if end and start and end <= start:
            return no_data_response()
        else:
            query, query_params = _sql_for_histograms(tags,
                                                      min(do.job.test_start for do in data_objects),
                                                      start,
                                                      end)
            result_csv = ClickhouseClient().select_csv(query, query_params=query_params, with_names=True)
            return HttpResponse(str(result_csv, encoding='utf8'), content_type='text/csv')


@data_getter
def stored_aggregates(request, data_objects, case_objects, tags):
    """
    :param request: django HTTP request
    :return:
    """
    start = request.GET.get('start') or request.GET.get('from')
    end = request.GET.get('end') or request.GET.get('to')
    try:
        start = int(start) if start else None
        end = int(end) if end else None
    except ValueError:
        return HttpResponseBadRequest('"start", "end", "from" and "to" must be numbers')
    from_aggregates, from_distributions = [case for case in case_objects if case.data.has_aggregates],\
                                          [case for case in case_objects if not case.data.has_aggregates]
    data_from_aggregates, data_from_distributions = None, None
    if len(from_aggregates) > 0:
        data_from_aggregates = get_stored_aggregates_from_aggregates([case.tag.hex for case in from_aggregates],
                                                                     min(case.data.job.test_start for case in from_aggregates),
                                                                     start, end)
    if len(from_distributions) > 0:
        data_from_distributions = get_stored_aggregates_from_distributions([case.tag.hex for case in from_distributions],
                                                                           min(case.data.job.test_start for case in from_distributions),
                                                                           start, end)
    result_df = pd.concat([df for df in [data_from_aggregates, data_from_distributions] if df is not None])
    return HttpResponse(result_df.to_csv(), content_type='text/csv')


@data_getter
def calc_aggregates(request, data_objects, case_objects, tags):
    """
    /get_aggregates/
    Ручка старого образца, она до сих пор используется на странице теста
    Ручки в приложении compare более универсальны и следует использовать именно их и тут и там.
    :param request: django HTTP request
    :return: django HTTP response
    """
    # FIXME: выпилить эту ручку в пользу ручки из приложения compare
    data_object = data_objects[0]
    tag = tags[0]
    if not data_object.type == 'metrics':
        return HttpResponseBadRequest('This handler is for metrics only. For events use /get_events/?tag=...')

    test_start = data_object.job.test_start
    offset = data_object.offset

    try:
        start = request.GET.get('start') or request.GET.get('from') or test_start + offset
        start = int(start)
        end = request.GET.get('end') or request.GET.get('to') or 0
        end = int(end)
    except ValueError:
        return HttpResponseBadRequest('"start", "end", "from" and "to" must be digital')

    ch_client = ClickhouseClient()

    if not end:
        end = ch_client.select("select max(ts) from metrics where tag = '{tag}'", query_params={'tag': tag})
        if end:
            end = int(end[0][0])
            # прибавлять и отнимать test_start+offset приходится потому, что с фронта будут приходить таймстемпы
            end += test_start + offset
        else:
            return HttpResponse('timestamp', content_type='text/csv')

    query, query_params = _sql_for_calc_aggregates(tag, start, end, test_start, offset)

    df = pd.read_csv(
        BytesIO(ch_client.select_csv(query, query_params=query_params)),
        names=[
            'timestamp', 'max', 'q99', 'q98', 'q95', 'q90', 'q85', 'q80', 'q75', 'q50', 'q25', 'q10', 'min'
        ]
    )

    # Заполняем пропуски на таймлайне пустыми значениями
    if df.index.size > 1:
        index_interval = int(pd.Series(df.index).diff().mode()[0])
        new_index = range(df.index[0], df.index[-1], index_interval)
        df = df.reindex(new_index, fill_value='', method='nearest', tolerance=index_interval)

    return HttpResponse(df.to_csv(index=False), content_type='text/csv')


def get_stored_aggregates_from_aggregates(tags, test_start, start, end):
    query, query_params = _sql_for_stored_aggregates_from_aggregates(tags, test_start, start, end)
    df = pd.read_csv(
        BytesIO(ClickhouseClient().select_csv(query, query_params=query_params)),
        names=['timestamp', 'tag', 'max', 'q99', 'q98', 'q95', 'q90', 'q85', 'q80', 'q75', 'q50', 'q25', 'q10', 'min'],
        index_col=('timestamp', 'tag')
    )
    return df


def get_stored_aggregates_from_distributions(tags, test_start, start, end):
    query, query_params = _sql_for_stored_aggregates_from_distributions(tags, test_start, start, end)
    distributions = pd.read_csv(
        BytesIO(ClickhouseClient().select_csv(query, query_params=query_params)),
        names=['ts', 'tag', 'l', 'r', 'cnt'],
    )
    if distributions.index.size > 1:
        index_interval = int(pd.Series(distributions.index).diff().mode()[0])
        new_index = range(distributions.index[0], distributions.index[-1], index_interval)
        distributions = distributions.reindex(new_index, fill_value=0, method='nearest', tolerance=index_interval)

    df = distributions.groupby(['ts', 'tag']).agg({'cnt': lambda x: list(x.cumsum()), 'l': list, 'r': list})
    df.index.rename(('timestamp', 'tag'), inplace=True)
    quantiles = [1.0, 0.99, 0.98, 0.95, 0.9, 0.85, 0.8, 0.75, 0.5, 0.25, 0.1, 0]
    names = ['max', 'q99', 'q98', 'q95', 'q90', 'q85', 'q80', 'q75', 'q50', 'q25', 'q10', 'min']
    result = pd.DataFrame(index=df.index, columns=names)
    for ts in df.index:
        series = df.loc[ts]
        if not series.cnt:
            continue
        total_cnt = series.cnt[-1]
        for quantile, name in zip(quantiles, names):
            find_value = quantile * total_cnt
            index = np.searchsorted(series.cnt, find_value)
            result.at[ts, name] = (series.l[index] + series.r[index]) / 2
    return result

@data_getter
def get_summary(request, data_objects, case_objects, tags):
    """
    Ручка старого образца, она до сих пор используется на странице теста
    Ручки в приложении compare более универсальны и следует использовать именно их и тут и там.
    :param request: django HTTP request
    :return: django HTTP response
    """
    # FIXME: выпилить эту ручку в пользу ручки из приложения compare
    try:
        start_ts = request.GET.get('start') or request.GET.get('from')
        start = int((int(start_ts) - data_objects[0].job.test_start) / 1000000) if start_ts else None
        end_ts = request.GET.get('end') or request.GET.get('to')
        end = int((int(end_ts) - data_objects[0].job.test_start) / 1000000) if end_ts else None
    except ValueError:
        return HttpResponseBadRequest('"start", "end", "from" and "to" must be digital')

    summaries_df = pd.concat([d.summary(start, end) for d in data_objects])
    return HttpResponse(summaries_df.to_csv(index=False), content_type='text/csv')


@data_getter
def get_distributions(request, data_objects, cases, tags):
    """
    :param request: django HTTP request
    :return:
    """
    try:
        offset = request.GET.get('interval_offset')
        interval = request.GET.get('interval')
    except ValueError:
        return HttpResponseBadRequest('"interval", and "interval_offset" must be digits')
    df = cc_get_distributions(tags, offset, interval)
    return HttpResponse(df.to_csv(index=False), content_type='text/csv')


def get_events(request):
    """
    ЭТО РУЧКА НОВОГО ОБРАЗЦА. ЕЁ ВЫКИДЫВАТЬ НЕ НАДО

    принимает в запросе:
    - tag: набор тэгов метрик
    - interval_offset: набор офсетов нужного интервала для каждого тэга
    - interval: длительность интервала одна на всех.

    - grep: фильтр по тексту

    соответствие тэга и interval_offset обеспечивается упорядоченностью списка
    например ?tag=t1&tag=t2&interval_offset=1&interval_offset=2
    при первой загрузке графиков офсеты не передаются и считаются равными нулю

    :param request: django HTTP request
    :return: django HTTP response
    """

    try:
        tags, interval_offsets, interval, index_from, length, ts, cases, grep = _extract_parameters_for_get_events(request)
    except AssertionError as aexc:
        return HttpResponseBadRequest(repr(aexc))
    except ValueError:
        return HttpResponseBadRequest('"interval_offset" must be digital')

    try:
        data_objects = _get_data_objects_for_events(tags)
    except AssertionError:
        return HttpResponseBadRequest('This handler is for events only. For metrics use /get_events/?tag=...')
    except Data.DoesNotExist:
        return HttpResponseBadRequest('invalid tag %s' % tags)

    ch_client = ClickhouseClient()
    frames = []
    for tag in tags:
        df = _get_data_for_tag(ch_client, data_objects[tag], tag, interval_offsets[tag], interval, cases, grep)
        frames.append(df)
    if frames:
        resulting_df = pd.concat(frames, axis=0)
        resulting_df.sort_values(['ts', 'tag'], ascending=[True, True], inplace=True)
    else:
        resulting_df = pd.DataFrame()

    resulting_df['data_index'] = range(len(resulting_df))
    # index_from может быть 0, поэтому явно проверяем его на None
    if index_from is not None and length:
        resulting_df = resulting_df[index_from:index_from + length]
    if ts:
        resulting_df = _filter_data_by_ts(resulting_df, ts, length)

    return HttpResponse(resulting_df.to_csv(index=False), content_type='text/csv')


def cc_get_distributions(tags, offset, interval):
    start_constraint = 'and ts*1e6 >= {offset} ' if offset is not None else ''
    end_constraint = 'and ts*1e6 <= {offset} + {interval} ' if offset is not None and interval is not None else ''
    tag_set = "'" + "','".join(tags) + "'"
    sql = '''
        SELECT
        l as left,
        r as right,
        sum(cnt) as count,
        count/(r-l) as density
        from distributions
        WHERE tag IN ({tags})
        ''' + \
          start_constraint + end_constraint + 'GROUP BY l, r'
    df = pd.read_csv(
        BytesIO(ClickhouseClient().select_csv(
            sql,
            query_params={'tags': tag_set,
                          'offset': offset,
                          'interval': interval})),
        names=[
            'left', 'right', 'count', 'density'
        ]
    )
    df = df.sort_values('left')
    df['cumulative'] = df['count'].cumsum()
    return df


def _sql_for_stored_aggregates_from_aggregates(tags, test_start, start, end):
    start_constraint = 'and (ts*1e6 + {test_start}) >= {start} ' if start is not None else ' '
    end_constraint = 'and (ts*1e6 + {test_start}) <= {end} ' if end is not None else ' '
    query = '''
            select 
                (ts*1e6 + {test_start}), tag, q100, q99, q98, q95, q90, q85, q80, q75, q50, q25, q10, q0
            from aggregates
            where tag in ({tags})
        ''' + \
        start_constraint + end_constraint + 'order by ts'
    return query, {
        'tags': str(tags).strip('[]'),
        'test_start': test_start,
        'start': start,
        'end': end,
    }


def _sql_for_stored_aggregates_from_distributions(tags, test_start, start, end):
    """

    :param tags: список запрашиваемых метрик
    :param start: начало запрашиваемого интервала (микросекунды int)
    :param end: конец запрашиваемого интервала (микросекунды int)
    :return:
    """

    start_constraint = 'AND (ts*1e6 + {test_start}) >=  {start} ' if start is not None else ' '
    end_constraint = 'AND (ts*1e6 + {test_start}) <= {end} ' if end is not None else ' '
    tags_string = "'" + "', '".join(tags) + "'"
    query = '''
            SELECT 
            ts*1000000 + {test_start} as t,
            tag,
            l as left, 
            r as right, 
            sum(cnt)
            from distributions
            WHERE tag IN ({tags})
            ''' + \
            start_constraint + end_constraint + 'GROUP BY tag, t, left, right ORDER BY t, left, right'
    return query, {
        'test_start': test_start,
        'tags': tags_string,
        'start': start,
        'end': end,
    }


def _sql_for_calc_aggregates(tag, start, end, test_start, offset):
    """

    :param tag: тэг метрики (str)
    :param start: начало запрашиваемого интервала (микросекунды int)
    :param end: конец запрашиваемого интервала (микросекунды int)
    :param test_start: время начала теста (микросекунды int)
    :param offset: офсет метрики (микросекунды int)
    :return:
    """
    query = '''
            with quantilesExact(0.10, 0.25, 0.50, 0.75, 0.80, 0.85, 0.90, 0.95, 0.98, 0.99)(value) as qq
            select 
                intDiv(toInt64(ts)+{test_start}+{offset}, 1000000)*1000000 as t, 
                max(value),
                qq[10], qq[9], qq[8], qq[7], qq[6], qq[5], qq[4], qq[3], qq[2], qq[1], 
                min(value)
            from metrics
            where tag = '{tag}'
            and ts >= toInt64({start}) - {test_start} - {offset}
            and ts <= toInt64({end}) - {test_start} - {offset}
            group by t
            order by t
        '''
    query_params = {
        'tag': tag,
        'start': start,
        'end': end,
        'test_start': test_start,
        'offset': offset,
        # 'compress_ratio': compress_ratio,
    }
    return query, query_params


def _sql_for_histograms(tags, test_start, start, end):
    """

    :param tag: тэг метрики (str)
    :param start: начало запрашиваемого интервала (микросекунды int)
    :param end: конец запрашиваемого интервала (микросекунды int)
    :param test_start: время начала теста (микросекунды int)
    :return:
    """

    start_constraint = 'and ts*1e6 >= {start} ' if start else ''
    end_constraint = 'and ts*1e6 <= {end} ' if end else ''
    query = '''
            select 
                (ts*1e6 + {test_start}) as timestamp, tag, category, sum(cnt) as value
            from histograms_buffer
            where tag in ({tags})
            ''' + \
            start_constraint + end_constraint + ' group by tag, ts, category order by ts'
    return query, {
        'tags': str(tags).strip('[]'),
        'test_start': test_start,
        'start': start,
        'end': end,
    }


def _dots_available(tag, start, end, test_start, offset):
    """

    :param tag: тэг метрики (str)
    :param start: начало запрашиваемого интервала (микросекунды int)
    :param end: конец запрашиваемого интервала (микросекунды int)
    :param test_start: время начала теста (микросекунды int)
    :param offset: офсет метрики (микросекунды int)
    :return: int
    """
    query = '''
        select count() 
        from metrics 
        where tag = '{tag}'
        and ts >= toInt64({start}) - {test_start} - {offset}
        and ts <= toInt64({end}) - {test_start} - {offset}
    '''
    query_params = {
        'tag': tag,
        'start': start,
        'end': end,
        'test_start': test_start,
        'offset': offset,
    }
    dots = ClickhouseClient().select(query, query_params=query_params)
    return int(dots[0][0]) if dots else 0


def _compress_ratio(start, end, dots_required, dots_available):
    """

    :param start: начало запрашиваемого интервала (микросекунды int)
    :param end: конец запрашиваемого интервала (микросекунды int)
    :param dots_required: количество точек, которое ожидает фронтенд
    :param dots_available: сколько точек в стрельбе
    :return: int
    """
    if dots_available <= dots_required:
        return 1
    else:
        # rate показывает сколько точек в секунде
        rate = dots_available // ((end - start) // 1000000 or 1) or 1
        return dots_available * (10 ** 6 // rate) // dots_required


def _get_data_objects_for_metrics(tags):
    data_objects = {}
    for tag in tags:
        data_obj = Data.objects.get(uniq_id=tag)
        assert data_obj.type == 'metrics', "This handler is for metrics only. For events use /get_events/?tag=..."
        data_objects[tag] = data_obj
    return data_objects


def _sql_for_get_events(tag, start, end, test_start, offset, cases, grep):
    """

    :param tag: тэг ивента
    :param start: начало запрашиваемого интервала (микросекунды int)
    :param end: конец запрашиваемого интервала (микросекунды int)
    :param test_start: время начала теста (микросекунды int)
    :param offset: офсет метрики (микросекунды int)
    :param grep: строка для поиска по логам
    :return:
    """
    children_tag_set = "'" + "','".join(children_tags) + "'"

    query = '''
        select '{tag}', toInt64(ts)+{test_start}+{offset} as t, value
        from events where tag in ({tags})
        and ts >= toInt64({start_ts})
    '''
    if end:
        query += ' and ts <= toInt64({end})'
    if grep:
        query += " and match(value,'(?i)({grep})')"

    query_params = {
        'tag': tag,
        'tags': children_tag_set,
        'start_ts': start,
        'end': end,
        'test_start': test_start,
        'offset': offset,
        'grep': grep,
    }

    return query, query_params


def _extract_parameters_for_get_events(req):
    tags = [escape_string(tag) for tag in req.GET.getlist('tag')]  # SECURITAY!
    interval_offsets = req.GET.getlist('interval_offset')
    interval = req.GET.get('interval')
    ts = req.GET.get('ts')
    index_from = req.GET.get('index_from')
    length = req.GET.get('length')
    cases = req.GET.get('cases')
    grep = req.GET.get('grep')

    assert tags, 'at least one "tag" is required'
    assert all(param is None or param.isdigit() for param in (interval, index_from, length, ts))
    '"interval", "ts", "index_from", "length" must be digital if present'
    interval = int(interval) if interval else None
    index_from = int(index_from) if index_from else None
    ts = int(ts) if ts else None
    length = int(length) if length else None
    interval_offsets = list(map(int, interval_offsets))
    if not interval_offsets:
        interval_offsets = [0] * len(tags)
    interval_offsets = dict(zip(tags, interval_offsets))
    assert len(interval_offsets) == len(tags), 'every "tag" must have a corresponding "interval_offset"'

    return tags, interval_offsets, interval, index_from, length, ts, cases, grep


def _get_data_objects_for_events(tags):
    data_objects = {}
    for tag in tags:
        data_obj = Data.objects.get(uniq_id=tag)
        assert data_obj.type == 'events'
        data_objects[tag] = data_obj
    return data_objects


def _get_data_for_tag(client, data_obj, tag, interval_offset, interval, cases, grep):
    data_offset = data_obj.offset
    test_start = data_obj.job.test_start
    start_ts = data_offset + interval_offset
    if interval is not None:
        interval = int(interval)
        end = start_ts + interval
    else:
        end = None
    sql, query_params = _sql_for_get_events(tag, start_ts, end, test_start, data_offset, cases, grep)

    return pd.read_csv(BytesIO(client.select_csv(sql, query_params=query_params)), names=['tag', 'ts', 'value'])


def _filter_data_by_ts(df, ts, length):
    """Нужно сохранить индексы в рамках всего интерала, но данные отдать от ts"""

    try:
        rows_with_ts = df.iloc[(df['ts'] - ts).abs().argsort()[:2]]
        data_indexes_with_ts = [row for row in rows_with_ts.data_index]
        first_ts_row_data_index = data_indexes_with_ts[0]
    except IndexError:
        first_ts_row_data_index = 0

    if length:
        index1 = first_ts_row_data_index - int(length / 2)
        # Если запрошенный ts первый для этого тэга, отдаем данные с начала тэга
        index1 = 0 if index1 < 0 else index1
        index2 = first_ts_row_data_index + int(length / 2) + 1
        selected_data_indexes = [elem for elem in range(index1, index2)]
        df = df[df.data_index.isin(selected_data_indexes)]  # нужно отфильтровать по true
    else:
        # Если вдруг длина не задана, отдаем все ряды с заданным таймстемпом
        df = df[df['ts'] == ts]
    return df
