# -*- coding: utf-8 -*-
"""
Created on Jul 26, 2013

@author: noob
"""

import logging
import re
import time
from datetime import datetime
from hashlib import md5
from json import dumps

from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseServerError
from django_yauth.decorators import yalogin_required
from common.models import Job, CustomUserReport
from common.util.meta import parse_duration
from common.util.clients import ClickhouseClient, CacheClient
from common.util.decorators import memoized_property, cached
from monitoring.models import Metric


@yalogin_required
def get_layout(request, job):
    cache = CacheClient()
    cache_key = 'job_{}_layout'.format(job)
    user = request.yauser
    job_obj = Job.objects.get(n=job)
    custom_reports = CustomUserReport.objects.filter(user=user.login, active=1)
    try:
        layout = cache.get(cache_key)
        if layout:
            try:
                if job_obj.mobile_data_key:
                    layout.append({'name': 'mobile',
                                   'title': 'Мобильные данные',
                                   'controls': get_mobile_controls(job_obj),
                                   })
            except:
                logging.exception('')
            if custom_reports:
                for cur in custom_reports:
                    layout.append({'name': 'custom__{}'.format(cur.n),
                                   'title': cur.name.split(':')[1].strip() or 'custom:{}'.format(cur.n),
                                   'controls': get_custom_controls(job_obj, cur)
                                   })
        else:
            raise KeyError()
    except KeyError:  # main part here
        test_data_controls = get_test_data_controls(job_obj)
        monitoring_controls = get_monitoring_controls(job_obj)

        layout = []

        if not job_obj.monitoring_only:
            layout.append({'name': 'test_data',
                           'title': 'Данные теста',
                           'controls': test_data_controls
                           })
        if monitoring_controls:
            layout.append({'name': 'monitoring',
                           'title': 'Мониторинг',
                           'controls': monitoring_controls
                           })
        cache.set(cache_key, layout)  # do not cache custom reports;
        try:
            if job_obj.mobile_data_key:
                layout.append({'name': 'mobile',
                               'title': 'Мобильные данные',
                               'controls': get_mobile_controls(job_obj),
                               })
        except:
            logging.exception('')
        if custom_reports:
            for cur in custom_reports:
                layout.append({'name': 'custom__{}'.format(cur.n),
                               'title': cur.name.split(':')[1].strip() or 'custom:{}'.format(cur.n),
                               'controls': get_custom_controls(job_obj, cur)
                               })

    except ObjectDoesNotExist:
        logging.warning('No such Job: {}'.format(job))
        return HttpResponseBadRequest
    except:
        logging.exception('Could not get layout for job {} due to:'.format(job))
        return HttpResponseServerError
    return HttpResponse(dumps(layout), content_type='application/json')


def get_test_data_controls(job_obj):
    """
    returns list of dicts with controls for monitoring tab layout
    slider, targers, metric_groups
    :param job_obj: Job OBJECT
    """
    try:
        slider = Slider(job_obj, 'test_data')
        if job_obj.multitag:
            delimiter = '|'
            tags = []
            for case in job_obj.cases:
                tags.extend(case.split(delimiter))
            cases = sorted(tag for tag in set(tags))
        else:
            cases = job_obj.cases
        test_data_layout = [{'name': 'slider',
                             'type': 'slider',
                             'min': slider.min,
                             'max': slider.max,
                             'start': slider.min,
                             'end': slider.max,
                             'helpers': slider.helpers
                             },
                            {'name': 'plot_groups',
                             'type': 'radio',
                             'default': 'main',
                             'values': (('main', 'Обзор теста'),
                                        ('extended', 'Расширенный анализ'),
                                        ('additional', 'Распределения и сводные'),
                                        ('tables', 'Таблицы'))
                             },
                            {'name': 'tags',
                             'type': 'radio',
                             'default': '',
                             'values': list(zip([''] + [md5(c.encode('utf-8')).hexdigest() for c in cases],
                                           ['Все тэги'] + cases))
                             },
                            ]
        return test_data_layout
    except:
        logging.exception('Could not get test_data_controls for job {} due to:'.format(job_obj.n))
        return None


def get_monitoring_controls(job_obj):
    """
    returns list of dicts with controls for monitoring tab layout
    slider, targers, metric_groups
    :param job_obj: Job OBJECT
    """
    try:
        monitoring_controls_processor = MonitoringControlsProcessor(job_obj)
        if not job_obj.monitoring_exists:
            return None
        else:
            slider = Slider(job_obj, 'monitoring')
            monitoring_controls = [{'name': 'slider',
                                    'type': 'slider',
                                    'min': slider.min,
                                    'max': slider.max,
                                    'start': slider.min,
                                    'end': slider.max,
                                    'helpers': slider.helpers
                                    },
                                   {'name': 'machines',
                                    'type': 'radio',
                                    'default': monitoring_controls_processor.default_target,
                                    'values': monitoring_controls_processor.targets
                                    },
                                   {'name': 'metrics',
                                    'type': 'radio',
                                    'default': monitoring_controls_processor.default_metrics_group,
                                    'values': list(zip([''] + monitoring_controls_processor.metrics_groups,
                                                  ['Все метрики'] + monitoring_controls_processor.metrics_groups))
                                    }]
            return monitoring_controls
    except:
        logging.exception('Could not get monitoring_controls for job {} due to:'.format(job_obj.n))
        return None


def get_mobile_controls(job_obj):
    """

    :param job_obj: Job OBJECT
    :return:
    """
    slider = Slider(job_obj, 'test_data')
    mobile_layout = [{'name': 'slider',
                      'type': 'slider',
                      'min': slider.min,
                      'max': slider.max,
                      'start': slider.min,
                      'end': slider.max,
                      'helpers': slider.helpers
                      },
                     {'name': 'mobile',
                      'type': 'radio',
                      'default': job_obj.mobile_data_key,
                      'values': [(job_obj.mobile_data_key, job_obj.mobile_data_key)]
                      },
                     ]
    return mobile_layout


def get_custom_controls(job_obj, cur):
    """

    :param job_obj: Job OBJECT
    :param cur: CustomUserReport OBJECT
    """
    slider = Slider(job_obj, 'test_data')
    if job_obj.multitag:
        delimiter = '|'
        tags = []
        for case in job_obj.cases:
            tags.extend(case.split(delimiter))
        cases = sorted(tag for tag in set(tags))
    else:
        cases = job_obj.cases
    custom_report_layout = [{'name': 'slider',
                             'type': 'slider',
                             'min': slider.min,
                             'max': slider.max,
                             'start': slider.min,
                             'end': slider.max,
                             'helpers': slider.helpers
                             },
                            {'name': 'plot_groups',
                             'type': 'radio',
                             'default': 'main',
                             'values': (('main', cur.name.split(':')[1].strip() or 'custom:{}'.format(cur.n)),)
                             },
                            {'name': 'tags',
                             'type': 'radio',
                             'default': '',
                             'values': list(zip([''] + [md5(c.encode('utf-8')).hexdigest() for c in cases], ['Все'] + cases)),
                             },
                            ]
    if job_obj.monitoring_exists and [p for p in cur.plots if p.startswith('monitoring_')]:
        monitoring_controls_processor = MonitoringControlsProcessor(job_obj)
        custom_report_layout.append({
            'name': 'machines',
            'type': 'radio',
            'default': monitoring_controls_processor.default_target,
            'values': monitoring_controls_processor.targets,
        })
    if job_obj.mobile_data_key and [p for p in cur.plots if p.startswith('mobile_')]:
        custom_report_layout.append({
            'name': 'mobile',
            'type': 'radio',
            'default': job_obj.mobile_data_key,
            'values': [(job_obj.mobile_data_key, job_obj.mobile_data_key)]
        })
    return custom_report_layout


def get_single_report_controls(request, job):
    """

    :param request: HTTP Request
    :param job: Job NUMBER
    """
    try:
        job_obj = Job.objects.get(n=job)
        cur_n = request.GET.get('cur', '0')
        cur = CustomUserReport.objects.get(n=cur_n)
        controls = {'name': 'custom__{}'.format(cur.n),
                    'title': cur.name.split(':')[1].strip() or 'custom:{}'.format(cur.n),
                    'controls': get_custom_controls(job_obj, cur)
                    }
        return HttpResponse(dumps(controls), content_type='application/json')
    except:
        logging.exception('')
        return HttpResponseBadRequest()


class MonitoringControlsProcessor(object):
    def __init__(self, job_obj):
        """

        :param job_obj: Job OBJECT
        """
        self.job_obj = job_obj
        self.ch_client = ClickhouseClient()

    @memoized_property
    def targets(self):
        """
        returns tuple of tuples
        """
        try:
            targets = tuple((target.n, target.host) for target in self.job_obj.targets)
            if len(targets) > 1:  # adding all targets tab
                targets = tuple(list(targets) + [(-1, 'Все мишени')])
            return targets
        except:
            logging.exception('Could not get targets for job {}. Taking job srv as target'.format(self.job_obj.n))
            targets = ((self.job_obj.srv.n, self.job_obj.srv.host),)
            return targets

    @property
    def default_target(self):
        """
        returns target's number
        """
        if self.job_obj.srv in self.job_obj.targets:
            try:
                default_target = self.job_obj.srv.n
                return default_target
            except:
                logging.exception('Could not get default_target for job {} due to:'.format(self.job_obj.n))
                return None
        else:
            default_target = self.targets[0][0]
            return default_target

    @memoized_property
    def metrics_groups(self):
        """
        returns list of unique metric groups for all targets
        """
        try:
            metrics = []
            for target in self.targets:
                metrics += set(self.get_target_metrics(target).keys())
            metrics_groups = []
            for metric in metrics:
                group = metric.split(':')[0].split('_')[0]
                if group == 'custom':
                    # looking for telegraf metrics
                    try:
                        if metric.split(':')[1].split('_')[0].startswith('cpu-cpu'):
                            group = 'CPU'
                        elif metric.split(':')[1].split('_')[0].startswith('diskio-'):
                            group = 'Disk'
                        elif metric.split(':')[1].split('_')[0].startswith('net-'):
                            group = 'Net'
                    except:
                        logging.exception('')
                metrics_groups.append(group)
            return ['Агрегаты'] + sorted(set(metrics_groups))
        except:
            logging.exception('Could not get metrics_groups for job {} due to:'.format(self.job_obj.n))
            return []

    @property
    def default_metrics_group(self):
        try:
            default_metrics_group = self.metrics_groups[0]
            return default_metrics_group
        except:
            logging.exception('Could not get default_metrics_group for job {} due to:'.format(self.job_obj.n))
            return None

    def get_target_metrics(self, target):
        """
        cached for all monitoring plots for this job
        returns dict where metric codes are keys, and ids are values
        :param target: tuple (Server.n, Server.host)
        """
        cache_key = 'job_{}_machine_{}_monitoring_metrics'.format(self.job_obj.n, target[0])

        @cached(cache_key)
        def target_metrics():
            sql = '''
                select distinct metric_name
                from loaddb.monitoring_verbose_data_buffer
                where job_id={job}
                and job_date=toDate({job_date})
                and target_host='{target}'
                '''
            query_params = self.job_obj.basic_query_params.copy()
            query_params['target'] = target[1]
            metric_ids = self.ch_client.select(sql, query_params=query_params)

            if metric_ids:
                metrics = {}
                for i in metric_ids:
                    metric_obj = Metric.objects.get_or_create(code=i[0])[0]
                    metrics[metric_obj.code] = metric_obj.id
            else:
                metrics = {}
            return metrics
        try:
            return target_metrics()
        except:
            logging.exception('Could not get monitoring metrics for job {} due to:'.format(self.job_obj.n))
            return {}


class Slider(object):
    def __init__(self, job_obj, slider_type):
        """

        :param job_obj: Job OBJECT
        :param slider_type: string 'monitoring' or 'test_data'
        """
        # input params
        self.slider_type = slider_type
        self.ch_client = ClickhouseClient()
        self.job_obj = job_obj

    @memoized_property
    def min(self):
        if self.slider_type == 'monitoring':
            sql_offline_min_time = '''select toUInt32(min(time))
                    from loaddb.monitoring_verbose_data_buffer
                    where job_id={job}
                    and job_date=toDate({job_date})
                    '''
            return int(self.ch_client.select(sql_offline_min_time, query_params=self.job_obj.basic_query_params)[0][0])
        else:
            return self.job_obj.data_started_unix

    @memoized_property
    def max(self):
        if self.slider_type == 'monitoring':
            sql_offline_max_time = '''select toUInt32(max(time))
                    from loaddb.monitoring_verbose_data_buffer
                    where job_id={job}
                    and job_date=toDate({job_date})
                    '''
            return int(self.ch_client.select(sql_offline_max_time, query_params=self.job_obj.basic_query_params)[0][0])
        else:
            return self.job_obj.data_stopped_unix

    @property
    def all_test(self):
        data = {'name': 'Весь тест',
                'start': self.min,
                'end': self.max}
        return data

    @memoized_property
    def job_scheme_type(self):
        """
        looks for scheme_type in memcached (rps or instances)
        asks database if there's no data in memcached
        returns dict with type and color for plot
        """
        cache_key = 'job_{}_scheme_type'.format(self.job_obj.n)

        @cached(cache_key)
        def fetch():
            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=self.job_obj.basic_query_params)
            nonzero_threads = 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 threads!=0
            ''', query_params=self.job_obj.basic_query_params)
            if nonzero_reqps:
                scheme_type = {'type': 'rps', 'color': '#800000'}
            elif not any([nonzero_reqps, nonzero_threads]):
                # for manual phout insertion
                scheme_type = {'type': 'rps', 'color': '#800000'}
            else:
                scheme_type = {'type': 'instances', 'color': '#ff00ff'}
            return scheme_type
        try:
            return fetch()
        except:
            logging.exception('Could not check reqps for job {} due to:'.format(self.job_obj.n))
            return {'type': 'rps', 'color': '#800000'}

    def get_constant_threads(self):
        """
        constant load parts of loadscheme
        based on trail requests per second to take in account long line loaschemes
        if any part is ended and then appears again (i.e. _-_ ) it will be considered as one big part
        """
        try:
            sql = '''select threads, toUInt32(min(time)), toUInt32(max(time)) from loaddb.rt_microsecond_details_buffer
                where job_id={job}
                and job_date=toDate({job_date})
                and tag=''
                group by threads
                having count()>5
                order by time'''
            constant_threads = self.ch_client.select(sql, query_params=self.job_obj.basic_query_params)
            constant_threads = [{'name': threads[0],
                                 'start': time.mktime(datetime.timetuple(threads[1])),
                                 'end': time.mktime(datetime.timetuple(threads[2]))} for threads in constant_threads]
            return constant_threads
        except:
            logging.exception('Could not retrieve constant threads for job {} due to:'.format(self.job_obj.n))
            return []

    def get_loadscheme_helpers(self, loadschemes):
        """

        """
        ls_helpers = []
        step_ls = []  # Хелперы для целых ступенчатых участков, указанных в схеме нагрузки.
        i = 0

        while i < len(loadschemes):
            loadscheme = loadschemes[i]
            if loadscheme.dsc.startswith('step'):
                step_params = [
                    int(re.sub('[^0-9]', '', p))  # keep digits only
                    for p in loadscheme.dsc.split(',')
                ]
                descending = step_params[0] > step_params[1]  # i.e. step(10,1,1,5)

                start = self.job_obj.data_started_unix + loadscheme.sec_from
                end = self.job_obj.data_started_unix + loadscheme.sec_to - 1

                # Обработка крайне редкого случая,
                # когда в схеме могут быть указаны два одинаковых ступенчатых участка.
                if descending:
                    start -= 1
                    end -= 1
                    possible_step_count = ((step_params[0] - step_params[1]) // step_params[
                        2]) + 1  # (higher rps minus lower rps) divided by leap + 1
                else:
                    possible_step_count = ((step_params[1] - step_params[0]) // step_params[
                        2]) + 1  # (higher rps minus lower rps) divided by leap + 1
                try:
                    st_ls = next((item for item in step_ls if
                             item['dsc'] == loadscheme.dsc and item['step_count'] < possible_step_count))
                    st_ls['end'] = self.job_obj.data_started_unix + loadscheme.sec_to - 1  # reset end of helper
                    st_ls['step_count'] += 1
                except StopIteration:
                    step_ls.append({
                        'step_count': 1,
                        # те участки, которые уже учтены, будут вставлены в общий список хелперов, смещая индекс.
                        'position': len(step_ls) + i,
                        'dsc': loadscheme.dsc,
                        'start': start,
                        'end': end,
                    })
                ls_helpers.append({
                    'name': loadscheme.load_from,
                    'start': start,
                    'end': end,
                })
            else:
                ls_helpers.append({
                    'name': loadscheme.dsc,
                    'start': self.job_obj.data_started_unix + loadscheme.sec_from,
                    'end': self.job_obj.data_started_unix + loadscheme.sec_to - 1,
                })
            i += 1
        # Вставляем хелпер для всего ступенчатого участка перед хелперами для отдельных ступенек.
        for ls in step_ls:
            ls_helpers.insert(ls['position'], {
                'name': ls['dsc'],
                'start': ls['start'],
                'end': ls['end'],
            })
        return ls_helpers

    def get_config_helpers(self):
        """
        warmup
        cooldown
        main_part
        from meta section in job configinfo
        """
        try:
            assert self.job_obj.configinfo
        except AssertionError:
            return []
        helpers = []
        warmup = parse_duration(self.job_obj.config.get('meta', {}).get('warmup'))
        cooldown = parse_duration(self.job_obj.config.get('meta', {}).get('cooldown'))
        if any([warmup, cooldown]):
            try:
                if cooldown < 0:
                    ls = self.job_obj.loadschemes
                    cooldown_start = ls[-1].sec_to - abs(cooldown) + 1 if ls else 0
                    start = self.job_obj.data_started_unix + warmup
                    end = self.job_obj.data_started_unix + cooldown_start - 1
                elif cooldown > 0:
                    start = self.job_obj.data_started_unix + warmup
                    end = self.job_obj.data_started_unix + cooldown - 1
                else:
                    start = self.job_obj.data_started_unix + warmup
                    end = self.job_obj.data_stopped_unix
                helpers.append({
                    'name': 'Основной участок',
                    'start': start,
                    'end': end,
                })
            except:
                logging.exception('')
        if warmup:
            try:
                helpers.insert(0, {
                    'name': 'Прогрев',
                    'start': self.job_obj.data_started_unix,
                    'end': self.job_obj.data_started_unix + warmup - 1,
                })
            except:
                logging.exception('')
        if cooldown:
            try:
                if cooldown < 0:
                    ls = self.job_obj.loadschemes
                    cooldown_start = ls[-1].sec_to - abs(cooldown) + 1 if ls else 0
                    start = self.job_obj.data_started_unix + cooldown_start
                    end = self.job_obj.data_stopped_unix
                else:
                    start = self.job_obj.data_started_unix + cooldown
                    end = self.job_obj.data_stopped_unix
                helpers.append({
                    'name': 'Кулдаун',
                    'start': start,
                    'end': end,
                })
            except:
                logging.exception('')
        return helpers

    @property
    def helpers(self):
        """
        returns list of helpers that control the slider
        """
        helpers = [self.all_test] + self.get_config_helpers() + self.get_loadscheme_helpers(self.job_obj.loadschemes)
        return helpers
