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

@author: noob
"""

from common.models import Job, CustomUserReport
from common.util.clients import ClickhouseClient, CacheClient
from common.util import parse_duration
from common.util.decorators import Memoize, approve_required
from monitoring.models import Metric
from datetime import datetime
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseServerError
from django.shortcuts import render_to_response
from django.template import RequestContext
from hashlib import md5
import json
import logging
import re
import time


# @login_required # let offlinepage be viewed by unauthorized users
# @approve_required
def get_layout_template(request, job):
    job_obj = Job.objects.get(n=job)
    return render_to_response('panel.html', {'job': job_obj}, RequestContext(request))


# @login_required # let offlinepage be viewed by unauthorized users
# @approve_required
def get_layout(request, job):
    cache = CacheClient()
    cache_key = 'job_%s_layout' % job
    user = request.user
    import os
    try:
        job_obj = Job.check_job(Job.objects.get(n=job))
        custom_reports = CustomUserReport.objects.filter(user=user.username, active=True)
        layout = cache.get(cache_key)
        if layout:
            logging.info('Found job %s layout in cache', job)
            if custom_reports:
                for cur in custom_reports:
                    layout.append({'name': 'custom__' + str(cur.n),
                                   'title': cur.name.split(':')[1].strip() or 'custom:%s' % cur.n,
                                   'controls': get_custom_controls(job_obj, cur)
                                   })
            return HttpResponse(json.dumps(layout), content_type='application/json')
        else:
            logging.info('No job %s layout in cache', job)
            raise KeyError()
    except KeyError:  # main part here
        monitoring_controls = get_monitoring_controls(job_obj, job_owner=(job_obj.user == request.user))
        test_data_controls = get_test_data_controls(job_obj)

        logging.debug('LAYOUT TESTDATA %s', test_data_controls)
        logging.debug('LAYOUT MONITORING %s', monitoring_controls)

        layout = []

        if not job_obj.monitoring_only:
            layout.append({'name': 'test_data',
                           'title': 'Responses',
                           'controls': test_data_controls
                           })
        if monitoring_controls:
            layout.append({'name': 'monitoring',
                           'title': 'Monitoring',
                           'controls': monitoring_controls
                           })
        cache.set(cache_key, layout)  # do not cache custom reports;
        if custom_reports:
            for cur in custom_reports:
                layout.append({'name': 'custom__' + str(cur.n),
                               'title': cur.name.split(':')[1].strip() or 'custom:%s' % cur.n,
                               'controls': get_custom_controls(job_obj, cur, job_owner=(job_obj.user == request.user))
                               })
        return HttpResponse(json.dumps(layout), content_type='application/json')
    except Job.DoesNotExist:
        logging.warning('No such Job: %s', job)
        return HttpResponseBadRequest
    except Job.Deleted:
        logging.warning('Job %s is deleted', job)
        return HttpResponse(json.dumps([{'success': 0, 'msg': 'Job had been deleted'}]), content_type='application/json')
    except:
        logging.exception('Could not get layout for job %s due to:', job)
        return HttpResponseServerError


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', u'Overview'),
                                        ('extended', u'Extended analysis'),
                                        ('additional', u'Distributions'),
                                        ('tables', u'Tables'))
                             },
                            {'name': 'tags',
                             'type': 'radio',
                             'default': '',
                             'values': zip([''] + [md5(c.encode('utf-8')).hexdigest() for c in cases],
                                           ['Overall'] + cases)
                             },
                            ]
        logging.info('Got test_data controls for job %s', job_obj.n)
        return test_data_layout
    except:
        logging.exception('Could not get test_data_controls for job %s due to:', job_obj.n)
        return None


def get_monitoring_controls(job_obj, job_owner=False):
    """
    returns list of dicts with controls for monitoring tab layout
    slider, targers, metric_groups
    :param job_obj: Job OBJECT
    :param job_owner: boolean
    """
    try:
        monitoring_controls_processor = MonitoringControlsProcessor(job_obj, job_owner=job_owner)
        if not job_obj.monitoring_exists:
            logging.info('No monitoring for job %s', job_obj.n)
            return None
        else:
            logging.debug('Mon EXISTS %s', job_obj.monitoring_exists)
            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': zip([''] + monitoring_controls_processor.metrics_groups,
                                                  [u'All metrics'] + monitoring_controls_processor.metrics_groups)
                                    }]
            logging.info('Got monitoring_controls for job %s', job_obj.n)
            return monitoring_controls
    except:
        logging.exception('Could not get monitoring_controls for job %s due to:', job_obj.n)
        return None


def get_custom_controls(job_obj, cur, job_owner=False):
    """

    :param job_obj: Job OBJECT
    :param cur: CustomUserReport OBJECT
    :param job_owner: boolean
    """
    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:%s' % cur.n),)
         },
        {'name': 'tags',
         'type': 'radio',
         'default': '',
         'values': zip([''] + [md5(c.encode('utf-8')).hexdigest() for c in cases],
                       ['Overall'] + cases),
         },
    ]
    if job_obj.monitoring_exists and [p for p in cur.plots if p.startswith('monitoring_')]:
        monitoring_controls_processor = MonitoringControlsProcessor(job_obj, job_owner=job_owner)
        custom_report_layout.append({
            'name': 'machines',
            'type': 'radio',
            'default': monitoring_controls_processor.default_target,
            'values': monitoring_controls_processor.targets,
        })
    return custom_report_layout


# @login_required # let offlinepage be viewed by unauthorized users
# @approve_required
def get_single_report_controls(request, job):
    """

    :param request: HTTP Request
    :param job: Job NUMBER
    """
    try:
        job_obj = Job.check_job(Job.objects.get(n=job))
        cur_n = request.GET.get('cur', '0')
        cur = CustomUserReport.objects.get(n=cur_n)
        controls = {
            'name': 'custom__' + str(cur.n),
            'title': cur.name.split(':')[1].strip() or 'custom:%s' % cur.n,
            'controls': get_custom_controls(job_obj, cur)
        }
        return HttpResponse(json.dumps(controls), content_type='application/json')
    except Job.Deleted:
        return HttpResponse(json.dumps({'success': 0, 'name': 'custom__0', 'title': 'Job had been deleted', 'controls': {}}), content_type='application/json')
    except Exception:
        logging.exception('Internal error when get single report controller.', exc_info=True)
        return HttpResponse(json.dumps({'success': 0, 'name': 'custom__0', 'title': 'Internal error', 'controls': {}}), content_type='application/json') 


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

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

    @property
    @Memoize
    def targets(self):
        """
        returns tuple of tuples
        """
        try:
            targets = tuple(
                [(target.n, target.host if self.job_owner else 'mysterious host %s' % target.n, target.dsc) for target
                 in self.job_obj.targets])
            if len(targets) > 1:  # adding all targets tab
                targets = tuple(list(targets) + [(-1, u'All targets', u'All targets')])
            logging.debug('Got targets for job %s: %s', self.job_obj.n, targets)
            return targets
        except:
            logging.exception('Could not get targets for job %s. Taking job srv as target', self.job_obj.n)
            targets = ((self.job_obj.srv.n, self.job_obj.srv.host, self.job_obj.srv.dsc),)
            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
                logging.debug("Taking job's %s srv as a default_target: %s", self.job_obj.n, default_target)
                return default_target
            except:
                logging.exception('Could not get default_target for job %s due to:', self.job_obj.n)
                return None
        else:
            default_target = self.targets[0][0]
            logging.debug('Picked default_target for job %s: %s', self.job_obj.n, default_target)
            return default_target

    @property
    @Memoize
    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())
            logging.info('Got metrics_groups for job %s', self.job_obj.n)
            metrics_groups = sorted([metric.split(':')[0].split('_')[0] for metric in metrics])
            return [u'Aggregates'] + list(set(metrics_groups))
        except:
            logging.exception('Could not get metrics_groups for job %s due to:', self.job_obj.n)
            return None

    @property
    def default_metrics_group(self):
        try:
            default_metrics_group = self.metrics_groups[0]
            logging.debug('Default monitoring metrics group for job %s is %s', self.job_obj.n, default_metrics_group)
            return default_metrics_group
        except:
            logging.exception('Could not get default_metrics_group for job %s due to:', self.job_obj.n)
            return None

    def get_target_metrics(self, target):
        """
        cache for all monitoring plots for this job
        returns dict where metric codes are keys, and ids are values
        :target: tuple (Server.n, Server.host)
        """
        try:
            cache_key = 'job_%s_machine_%s_monitoring_metrics' % (self.job_obj.n, target[0])
            metrics = self.cache.get(cache_key)
            if metrics:
                logging.info('Found monitoring metrics for job %s, machine %s in cache', self.job_obj.n, target[0])
                return metrics
            else:
                logging.info('No monitoring metrics for job %s, machine %s in cache', self.job_obj.n, target[0])
                sql = '''
                    select distinct metric_name
                    from loaddb.monitoring_verbose_data_buffer
                    where job_id=%(job)s 
                    and job_date=toDate(%(job_date)s)
                    and target_host='%(target)s'
                '''
                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 = {}
                logging.debug('Got monitoring metrics for job %s, machine %s: %s', self.job_obj.n, target, metrics)
                self.cache.set(key=cache_key, val=metrics)
                return metrics
        except:
            logging.exception('Could not get monitoring metrics for job %s due to:', 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

        # meta params
        self.cache = CacheClient()

    @property
    @Memoize
    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)s
                
            '''
            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

    @property
    @Memoize
    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)s
                
            '''
            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': 'Overall',
                'start': self.min,
                'end': self.max}
        logging.debug('Got all_test for job %s: %s', self.job_obj.n, data)
        return data

    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)s 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.copy())
            logging.info('Got constant threads for job %s: %s', self.job_obj.n, len(constant_threads))
            constant_threads = [{'name': threads[0],
                                 'start': time.mktime(datetime.timetuple(threads[1])),
                                 'end': time.mktime(datetime.timetuple(threads[2]))} for threads in constant_threads]
            logging.debug('constant threads for job %s: %s', self.job_obj.n, constant_threads)
            return constant_threads
        except:
            logging.exception('Could not retrieve constant threads for job %s due to:', 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 = (item for item in step_ls if
                             item['dsc'] == loadscheme.dsc and item['step_count'] < possible_step_count).next()
                    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_duration
        cooldown_value
        main_part
        from meta section in job configinfo
        """
        try:
            assert self.job_obj.configinfo
        except AssertionError:
            return []
        helpers = []
        warmup = re.search(r'\nwarmup=[0-9dhms]+\n', self.job_obj.configinfo.replace(' ', ''))
        cooldown = re.search(r'\ncooldown=-?[0-9dhms]+\n', self.job_obj.configinfo.replace(' ', ''))
        if any([warmup, cooldown]):
            try:
                warmup_duration = re.findall(r'warmup=[0-9dhms]+',
                                             self.job_obj.configinfo.replace(' ', ''))  # number of seconds
                warmup_duration = warmup_duration[0] if warmup_duration else 0
                warmup_duration = parse_duration(warmup_duration.split('=')[1]) if warmup_duration else 0
                cooldown_value = re.findall(r'cooldown=-?[0-9dhms]+',
                                            self.job_obj.configinfo.replace(' ', ''))  # number of seconds
                cooldown_value = cooldown_value[0] if cooldown_value else 0
                cooldown_value = parse_duration(cooldown_value.split('=')[1]) if cooldown_value else 0
                if cooldown_value < 0:
                    ls = self.job_obj.loadschemes[::-1]
                    cooldown_start = ls[0].sec_to - abs(cooldown_value) + 1 if ls else 0
                    start = self.job_obj.data_started_unix + warmup_duration
                    end = self.job_obj.data_started_unix + cooldown_start - 1
                elif cooldown_value > 0:
                    start = self.job_obj.data_started_unix + warmup_duration
                    end = self.job_obj.data_started_unix + cooldown_value - 1
                else:
                    start = self.job_obj.data_started_unix + warmup_duration
                    end = self.job_obj.data_stopped_unix
                helpers.append({
                    'name': u'Main part',
                    'start': start,
                    'end': end,
                })
            except:
                logging.exception('')
        if warmup:
            try:
                helpers.insert(0, {
                    'name': u'Warmup',
                    'start': self.job_obj.data_started_unix,
                    'end': self.job_obj.data_started_unix + warmup_duration - 1,
                })
            except:
                logging.exception('')
        if cooldown and cooldown_value:
            try:
                if cooldown_value < 0:
                    ls = self.job_obj.loadschemes[::-1]
                    cooldown_start = ls[0].sec_to - abs(cooldown_value) + 1 if ls else 0
                    start = self.job_obj.data_started_unix + cooldown_start
                    end = self.job_obj.data_stopped_unix
                elif cooldown_value > 0:
                    start = self.job_obj.data_started_unix + cooldown_value
                    end = self.job_obj.data_stopped_unix
                helpers.append({
                    'name': u'Cooldown',
                    'start': start,
                    'end': end,
                })
            except:
                logging.exception('')
        return helpers

    @property
    def helpers(self):
        """
        returns list of helpers that control the slider
        looks if job was made using instances and returns only all_test helper in this case
        """
        if self.job_obj.scheme_type == 'rps':
            helpers = [self.all_test] + self.get_config_helpers() + self.get_loadscheme_helpers(
                self.job_obj.loadschemes)
            logging.debug('Helpers are: %s', helpers)
        else:
            helpers = []
            # TODO: for threads too
            # helpers = [self.all_test] + self.get_constant_threads()
        return helpers
