# -*- coding: utf-8 -*-
"""
Created on Feb 28, 2014

@author: noob
"""
import logging
import re
from collections import OrderedDict
from copy import deepcopy


from common.util.meta import xss_unescape, parse_duration
from common.util.clients import ClickhouseClient
from common.util.decorators import memoized_property
from comparepage.util import get_job_objects


class ComparePlot(object):
    """
    Plots and Tables Superclass
    """

    table = False
    compress_ratio = 1
    title = ''
    xaxis_type = 'linear'
    xaxis_label = ''
    yaxis_label = ''
    overrides = {}
    selectors = None

    def __init__(
            self,
            jobs,
            mainjob=None, helper='all', selector='',  # plot params
            case=None,  # trail params
            target=None, custom_metric=None  # monitoring params
    ):
        """

        :param jobs: job NUMBERS like '231234,123123'
        :param case: string
        :param custom_metric: string
        :param mainjob:
        :param helper:
        :param selector:
        :param target:
        """

        # input params
        self.jobs = jobs
        self.case = xss_unescape(case) if case else case
        self.custom_metric = custom_metric
        self.target = target
        self.helper = helper
        self.mainjob = mainjob
        self.selector = selector
        # meta attributes
        if not self.mainjob and self.jobs:
            self.mainjob = self.jobs.split(",")[0]

        self.ch_client = ClickhouseClient()
        if self.selectors and self.selector not in [str(s[0]) for s in self.selectors]:
            self.selector = self.default_selector

    @memoized_property
    def job_objects(self):
        return get_job_objects(self.jobs.split(","))

    @property
    def tag(self):
        """
        alias
        """
        return self.case

    @property
    def subtitle(self):
        subtitle = []

        if self.helper != 'all':
            name = self.helper.split('|')[2]
            if name.isdigit():
                name += ' reqps'
            subtitle.append(name)

        if self.case:
            case = 'Гильза: "' + self.case + '"'
            subtitle.append(case)
        return ' | '.join(subtitle)

    @staticmethod
    def color_mapping(index):
        """
        uses default highcharts colors
        from ver. 3.0.10
        """
        v3_colors = ['#2f7ed8',
                     #                      '#0d233a',
                     '#f28f43',
                     '#8bbc21',
                     '#910000',
                     '#1aadce',
                     '#492970',
                     '#77a1e5',
                     '#c42525',
                     '#a6c96a']
        return v3_colors[index % len(v3_colors)]

    @memoized_property
    def intervals(self):
        """
        returns dict of dicts
        start and end time for all jobs (depends on helper)
        """
        intervals = {}
        for job_obj in self.job_objects:
            try:
                intervals[job_obj.n] = {}
                if self.helper == 'all' and job_obj.monitoring_only:
                    sql = '''
                        select toUInt32(min(time)), toUInt32(max(time))
                        from loaddb.monitoring_verbose_data_buffer
                        where job_id={job}
                        and job_date=toDate({job_date})
                        '''
                    extremes = self.ch_client.select(sql, query_params=job_obj.basic_query_params)
                    if extremes:
                        start, end = extremes[0]
                    else:
                        start, end = job_obj.data_started_unix, job_obj.data_stopped_unix
                elif self.helper == 'all':
                    start, end = job_obj.data_started_unix, job_obj.data_stopped_unix
                elif self.helper.split('|')[2] == 'warmup':
                    warmup = parse_duration(job_obj.config.get('meta', {}).get('warmup'))
                    start, end = job_obj.data_started_unix, job_obj.data_started_unix + warmup
                elif self.helper.split('|')[2] == 'cooldown':
                    cooldown = parse_duration(job_obj.config.get('meta', {}).get('cooldown'))
                    if cooldown < 0:
                        ls = job_obj.loadchemes
                        cooldown_start = ls[-1][1] - abs(cooldown) + 1 if ls else 0
                        start, end = job_obj.data_started_unix + cooldown_start, job_obj.data_stopped_unix
                    else:
                        start, end = job_obj.data_started_unix + cooldown, job_obj.data_stopped_unix
                elif self.helper.split('|')[2] == 'main_part':
                    warmup = parse_duration(job_obj.config.get('meta', {}).get('warmup'))
                    cooldown = parse_duration(job_obj.config.get('meta', {}).get('cooldown'))
                    if cooldown < 0:
                        ls = job_obj.loadchemes
                        cooldown_start = ls[-1][1] - abs(cooldown) + 1 if ls else 0
                        start, end = job_obj.data_started_unix + warmup, job_obj.data_started_unix + cooldown_start - 1
                    elif cooldown > 0:
                        start, end = job_obj.data_started_unix + warmup, job_obj.data_started_unix + cooldown - 1
                    else:
                        start, end = job_obj.data_started_unix + warmup, job_obj.data_stopped_unix
                else:
                    start = job_obj.data_started_unix + int(self.helper.split('|')[0])
                    end = job_obj.data_started_unix + int(self.helper.split('|')[1]) - 1
                intervals[job_obj.n]['start'] = start
                intervals[job_obj.n]['end'] = end
            except:
                logging.exception('Could not get time intervals for job {} due to:'.format(job_obj.n))
        return intervals

    @staticmethod
    def count_percentage(count_data):
        """
        returns list
        :param count_data: list or gen of count
        """
        count_data = [value if value else 0 for value in count_data]
        summa = float(sum(count_data)) or 1.0
        percent_values = [round(float(value) * 100 / summa, 3) for value in count_data]
        return percent_values

    @property
    def default_selector(self):
        if self.selectors:
            return str(self.selectors[0][0])
        else:
            return None


class CompareTable(ComparePlot):
    """
    Upper class for all comparepage tables
    """
    table = True
    rows = {}
    dimension = ''
    yaxis = None
    xaxis_type = 'linear'

    def get_data(self):
        data = {
            'row_keys': [str(r) for r in list(self.rows.keys())],
            'rows': self.rows,
            'columns': self.columns_data,
            'dim': self.dimension,
            'selectors': self.selectors,
            'default_selector': self.default_selector,
        }
        return data

    @property
    def common_cases(self):
        import itertools
        return [''] + sorted(set(itertools.chain.from_iterable(j.cases for j in self.job_objects)))

    @property
    def columns_data(self):
        """
        adding difference depending on mainjob
        """
        columns_diff = deepcopy(list(self.columns_data_nodiff.values()))
        mainjob_index = [job.n for job in self.job_objects].index(int(self.mainjob))
        for row in columns_diff[1:]:
            for i in range(len(self.job_objects)):
                if not i == mainjob_index:
                    sign = ''
                    try:
                        assert row[mainjob_index] != row[i]
                        sign += '+' if row[mainjob_index] < row[i] else '-'
                        diff = sign + str(round(float(abs(row[mainjob_index] - row[i])), 3))
                    except AssertionError:
                        diff = '0'
                    row[i] = str(row[i]) + 'diff:' + diff
            row[mainjob_index] = str(row[mainjob_index]) + 'diff:'
        return OrderedDict(zip(list(self.rows.keys()), columns_diff))


class DistComparePlot(ComparePlot):
    """
    Upper class for all vs plots
    """

    @property
    def overrides(self):
        """
        returns dict of data to override default plot parameters
        if necessary, elements are added separately for each Plot subclass
        """
        overrides = {}  # super(DistComparePlot, self).overrides
        if 'xAxis' not in list(overrides.keys()):
            overrides['xAxis'] = {}
        overrides['xAxis']['gridLineWidth'] = 1
        if self.ticks > 15:
            overrides['xAxis']['tickInterval'] = self.ticks // 20
        return overrides

    @property
    def data_values(self):
        return []

    def get_data(self):
        try:
            data = {
                'xaxis': self.xaxis_points,
                'xaxis_label': self.xaxis_label,
                'yaxis': self.yaxis,
                'series': self.series_data,
                'selectors': self.selectors,
                'default_selector': self.default_selector,
            }
            return data
        except:
            logging.exception('Could not get data for {} for jobs {} due to:'.format(self.__class__.__name__,
                              [job.n for job in self.job_objects]))
            return None

    @memoized_property
    def ticks(self):
        """
        quantity of points on xAxis
        """
        return len(self.xaxis_points)


class TimelineComparePlot(ComparePlot):
    """
    Upper class for all timeline plots
    """
    xaxis_type = 'linear'

    def get_data(self):
        try:
            data = {
                'xaxis': self.xaxis_points,
                'xaxis_label': self.xaxis_label,
                'yaxis': self.yaxis,
                'series': self.series_data,
                'selectors': self.selectors,
                'default_selector': self.default_selector,
            }
            return data
        except:
            logging.exception('Could not get data for {} for jobs {} due to:'.format(self.__class__.__name__,
                              [job.n for job in self.job_objects]))
            return None

    @memoized_property
    def series_data(self):
        series_data = []
        if not self.selector:
            self.selector = self.default_selector
        database_data = []
        for job_obj in self.job_objects:
            serie_data = self.fetch_data(job_obj)
            database_data.append(bool(serie_data))
            serie = {
                'yAxis': 0,
                'marker': {
                    'enabled': False,
                    'states': {
                        'hover': {
                            'enabled': False,
                        },
                    },
                },
                'color': self.color_mapping(self.job_objects.index(job_obj)),
                'name': str(job_obj.n) + ' ' + str(job_obj.name),
                'data': serie_data,
                'label': self.yaxis_label,
            }
            series_data.append(serie)
            scheme_serie = {
                'yAxis': 1,
                'marker': {
                    'enabled': False,
                    'states': {
                        'hover': {
                            'enabled': False,
                        },
                    },
                },
                'connectNulls': self.scheme_types[job_obj.n] == 'threads',
                'color': self.color_mapping(self.job_objects.index(job_obj)),
                'name': self.scheme_types[job_obj.n],
                'data': self.scheme_data[job_obj.n],
                'dashStyle': 'ShortDot',
                'label': '',
            }
            series_data.append(scheme_serie)
        if not any(database_data):
            return None
        return series_data

    @property
    def yaxis(self):
        yaxis = [{
            'plotLines': [{
                'color': '#808080',
                'value': 0,
                'width': 1
            }],
            'showFirstLabel': False,
            'showLastLabel': False,
            'labels': {
                'align': 'left',
                'x': 12,
                'y': -4,
            },
            'title': {
                'text': '',
            },
            'min': 0,
            'allowDecimals': False,
            'label': self.yaxis_label,
        }]
        if not all(j.monitoring_only for j in self.job_objects):
            yaxis.append({
                'plotLines': [{
                    'color': '#808080',
                    'value': 0,
                    'width': 1
                }],
                'showFirstLabel': False,
                'showLastLabel': False,
                'labels': {
                    'align': 'right',
                    'x': -12,
                    'y': -4,
                },
                'gridLineWidth': 0,
                'opposite': True,
                'label': 'rps' if len(list(set(self.scheme_types.values()))) == 1 and
                list(set(self.scheme_types.values()))[0] == 'reqps' else '',
                'title': {
                    'text': '',
                },
                'min': 0,
                'allowDecimals': False,
            })
        return yaxis

    @property
    def xaxis_points(self):
        return [i for i in range(int(self.slider_length // self.compress_ratio))]

    @memoized_property
    def slider_length(self):
        slider_length = int(max(
            [self.intervals[job_obj.n]['end'] - self.intervals[job_obj.n]['start']
             for job_obj in self.job_objects]
        )
        )
        return slider_length

    @property
    def compress_ratio(self):
        compress_ratio = self.slider_length // 500 or 1
        return compress_ratio

    @property
    def overrides(self):
        overrides = {'xAxis': {
            'categories': self.xaxis_points,
            'labels': {
                'enabled': False,
            },
            'tickWidth': 0,
        }}
        return overrides

    @memoized_property
    def scheme_types(self):
        scheme_types = {}
        for job_obj in self.job_objects:
            try:
                nonzero_reqps = self.ch_client.select('''
                    select any(job_id)
                    from loaddb.rt_microsecond_details_buffer
                    where job_id={job}
                    and job_date=toDate({job_date})
                    and reqps!=0
                ''', query_params=job_obj.basic_query_params)
                if nonzero_reqps:
                    scheme_type = 'reqps'
                else:
                    scheme_type = 'threads'
            except:
                logging.exception('Could not check reqps for job {} due to:'.format(job_obj.n))
                scheme_type = 'rps'
            scheme_types[job_obj.n] = scheme_type
        return scheme_types

    @memoized_property
    def scheme_data(self):
        """
        gets scheme data (made in one method not to rape db)
        """
        scheme_data = {}
        for job_obj in self.job_objects:
            sql = '''select avg({scheme_type})
                from loaddb.rt_microsecond_details_buffer
                where job_id={job}
                and job_date=toDate({job_date})
                and tag = ''
                and time >= toDateTime({start})
                and time <= toDateTime({end})
                group by intDiv(toUInt32(time), {compress_ratio})*{compress_ratio}
                order by intDiv(toUInt32(time), {compress_ratio})*{compress_ratio}'''
            query_params = job_obj.basic_query_params.copy()
            query_params.update({
                'scheme_type': self.scheme_types[job_obj.n],
                'start': self.intervals[job_obj.n]['start'],
                'end': self.intervals[job_obj.n]['end'],
                'compress_ratio': self.compress_ratio
            })
            fetched_data = self.ch_client.select(sql, query_params=query_params)
            # processing raw data
            try:
                if self.scheme_types[job_obj.n] == 'threads':
                    scheme_data[job_obj.n] = [value[0] if value[0] else None for value in fetched_data]
                else:
                    scheme_data[job_obj.n] = [value[0] if value[0] else 0 for value in fetched_data]
            except:
                logging.exception('Could not get scheme_data for job {} due to:'.format(job_obj.n))
                scheme_data[job_obj.n] = []
        return scheme_data
