# -*- coding: utf-8 -*-
"""
Created on Jan 24, 2014

@author: noob
"""

import logging
import re
from collections import OrderedDict


from django.core.exceptions import ObjectDoesNotExist

from common.models import Component, Job, KPI, JobTrail
from common.util.aggregators import MonitoringAggregator, \
    RTDetailsAggregator, ProtoCodesAggregator, NetCodesAggregator, RTHistogramsAggregator
from common.util.clients import ClickhouseClient
from common.util.decorators import memoized_property


class JobSLAProcessor(object):
    """
    used for /api/job/(\d+)/sla.json
    """

    def __init__(self, job_obj):
        """

        :param job_obj: Job OBJECT
        """
        self.job_obj = job_obj

    @property
    def kpis(self):
        try:
            kpis = KPI.objects.filter(component_id=self.job_obj.component)
            return kpis
        except:
            logging.exception('Could not get kpis for job {} due to:'.format(self.job_obj.n))
            return KPI.objects.none()

    def check(self):
        try:
            kpi_resolutions = []
            failed_kpis = {}
            for kpi_obj in self.kpis:
                try:
                    kpi_sla_processor = kpi_sla_processor_map[kpi_obj.ktype.split('__')[0]](kpi_obj, self.job_obj)
                    kpi_res = kpi_sla_processor.check()
                    kpi_resolutions.append(kpi_res)
                    if kpi_res is False:
                        failed_kpis[str(kpi_obj.n)] = {}
                        failed_kpis[str(kpi_obj.n)]['failed_values'] = {}
                        for value in list(kpi_sla_processor.sla_results.keys()):
                            if not kpi_sla_processor.sla_results[value]:
                                failed_kpis[str(kpi_obj.n)]['failed_values'][value] = kpi_obj.params['sla'][value]
                                failed_kpis[str(kpi_obj.n)]['failed_values'][value]['actual'] = \
                                    kpi_sla_processor.data_values[value]
                        failed_kpis[str(kpi_obj.n)]['dsc'] = KPI.kpitype_map.get(kpi_obj.ktype, '')
                        failed_kpis[str(kpi_obj.n)]['type'] = str(kpi_obj.ktype)
                except:
                    logging.exception('')
            if all(res is not False for res in kpi_resolutions):
                return {'resolution': True}
            else:
                return {'resolution': False, 'failed_kpis': failed_kpis}

        except:
            logging.exception('')
            return None


class ProjectSLAProcessor(object):
    """
    Used for "My projects" block on mainpage and projectpage
    """

    def __init__(self, tag):
        self.tag = tag

    @property
    def components(self):
        try:
            return Component.objects.filter(tag=self.tag)
        except ObjectDoesNotExist:
            return Component.objects.none()

    def check(self):
        failed_components = []
        failed_services = []
        verdict = None
        try:
            for component_obj in self.components:
                kpis = KPI.objects.filter(component_id=component_obj.n)
                kpi_sla_results = []
                for kpi_obj in kpis:
                    if isinstance(kpi_obj.params, dict):
                        try:
                            kpi_sla_processor = kpi_sla_processor_map[kpi_obj.ktype.split('__')[0]](kpi_obj)
                            kpi_sla_results.append(kpi_sla_processor.check())
                        except:
                            logging.exception('')
                if False in kpi_sla_results:
                    failed_components.append(component_obj.name)
                    failed_services.extend(component_obj.services)
                elif True in kpi_sla_results:
                    verdict = 'green'
            if failed_components:
                verdict = 'red'
        except ObjectDoesNotExist:
            pass
        except:
            logging.exception('Could not check SLA for project {} due to:'.format(self.tag))
        finally:
            failed_components = ' | '.join(failed_components)
            failed_services = set(failed_services)
        return {'resolution': verdict, 'failed_components': failed_components, 'failed_services': failed_services}


class KPISLAProcessor(object):
    def __init__(self, kpi_obj, job_obj=None):
        """

        :param kpi_obj:
        :param job_obj:
        """
        self.kpi_obj = kpi_obj
        self.job_obj = job_obj

    def check(self):
        if not self.tracked_values or not self.latest_job:
            return None
        else:
            if False in list(self.sla_results.values()):
                return False
            else:
                return True

    @property
    def param_type(self):
        try:
            param_type = self.kpi_obj.ktype.split('__')[1]
        except IndexError:
            param_type = ''
        return param_type

    @memoized_property
    def component_obj(self):
        try:
            return Component.objects.get(n=self.kpi_obj.component_id)
        except:
            logging.exception('')
            return None

    @memoized_property
    def latest_job(self):
        if self.job_obj:
            return self.job_obj
        else:
            try:
                latest_job = Job.objects.filter(component=self.kpi_obj.component_id, td__isnull=False)
                if self.component_obj.job_order == 'ver_ordered':
                    latest_job = latest_job.order_by('-ver', '-n')[0]
                else:
                    latest_job = latest_job.order_by('-n')[0]
                return latest_job
            except:
                logging.exception('')
                return Job.objects.none()

    @memoized_property
    def tracked_values(self):
        """
        values like minimum or stddev which user wants to be controlled by sla.
        """
        tracked_values = []
        try:
            tracked_values = [
                key
                for key in list(self.kpi_obj.params['sla'].keys())
                if self.kpi_obj.params['sla'].get(key, {}).get('operator')
                ]
        except TypeError:
            logging.error('Error getting KPI {} params'.format(self.kpi_obj.n))
        except Exception:
            logging.exception('Error getting KPI %s params due to: ', self.kpi_obj.n, exc_info=True)
        return tracked_values

    @property
    def data_values(self):
        raise NotImplementedError()

    @property
    def sla_results(self):
        """
        returns dict
        sla results on every value
        """
        sla_results = {}
        try:
            kpi_sla = self.kpi_obj.params['sla']
            for value in self.tracked_values:
                if kpi_sla[value]['operator'] == '<':
                    sla_results.update({value: bool(self.data_values.get(value, 0) < kpi_sla[value]['threshold'])})
                elif kpi_sla[value]['operator'] == '<=':
                    sla_results.update({value: bool(self.data_values.get(value, 0) <= kpi_sla[value]['threshold'])})
                elif kpi_sla[value]['operator'] == '>':
                    sla_results.update({value: bool(self.data_values.get(value, 0) > kpi_sla[value]['threshold'])})
                elif kpi_sla[value]['operator'] == '>=':
                    sla_results.update({value: bool(self.data_values.get(value, 0) >= kpi_sla[value]['threshold'])})
                else:
                    pass

        except:
            logging.exception('Could not check kpi_sla for kpi # {}, due to:'.format(self.kpi_obj.n))
        return sla_results


class ImbalanceKPISLAProcessor(KPISLAProcessor):
    @property
    def data_values(self):
        return {'imbalance': self.latest_job.imbalance}

    @property
    def sla_results(self):
        """
        returns dict
        sla results on every value
        """
        sla_results = {}
        try:
            assert self.data_values.get('imbalance')
            kpi_sla = self.kpi_obj.params['sla']
            for value in self.tracked_values:
                if kpi_sla[value]['operator'] == '<':
                    sla_results.update({value: bool(self.data_values.get(value, 0) < kpi_sla[value]['threshold'])})
                elif kpi_sla[value]['operator'] == '<=':
                    sla_results.update({value: bool(self.data_values.get(value, 0) <= kpi_sla[value]['threshold'])})
                elif kpi_sla[value]['operator'] == '>':
                    sla_results.update({value: bool(self.data_values.get(value, 0) > kpi_sla[value]['threshold'])})
                elif kpi_sla[value]['operator'] == '>=':
                    sla_results.update({value: bool(self.data_values.get(value, 0) >= kpi_sla[value]['threshold'])})
                else:
                    pass
        except AssertionError:
            for value in self.tracked_values:
                sla_results.update({value: True})
        except:
            logging.exception('Could not check kpi_sla for kpi # {}, due to:'.format(self.kpi_obj.n))
        return sla_results


class QuantilesKPISLAProcessor(KPISLAProcessor):
    @property
    def data_values(self):

        try:
            assert not self.kpi_obj.params.get('case', '')
            return JobTrail.objects.get(up=self.latest_job).__dict__
        except (AssertionError, ObjectDoesNotExist):
            ch_client = ClickhouseClient()
            sql = '''
                select
                quantilesExactWeighted(0.50, 0.75, 0.80, 0.85, 0.90, 0.95, 0.98, 0.99)(bin, cnt)
                from loaddb.rt_microsecond_histograms_buffer
                where job_id={job}
                and job_date=toDate({job_date})
                and tag='{tag}'
            '''
            query_params = self.latest_job.basic_query_params.copy()
            query_params['tag'] = self.kpi_obj.params.get('case', '')
            quantiles_data = ch_client.select(sql, query_params=query_params)
            data = [float(q) / 1000 for q in quantiles_data[0][0]] \
                if quantiles_data else [0] * len(self.kpi_obj.params['graphs'])
            return dict(zip(['q50', 'q75', 'q80', 'q85', 'q90', 'q95', 'q98', 'q99'], data))
        except:
            logging.exception('')
            return dict(zip(['q50', 'q75', 'q80', 'q85', 'q90', 'q95', 'q98', 'q99'], [0] * 8))


class RTDetailsKPISLAProcessor(KPISLAProcessor):
    @memoized_property
    def data_values(self):
        try:
            aggregator = RTDetailsAggregator(self.latest_job, self.kpi_obj.params['case'])
            trail_aggregate = aggregator.get_param_data(self.param_type)
            trail_aggregate = {
                'average': trail_aggregate[0],
                'stddev': trail_aggregate[1],
                'minimum': trail_aggregate[2],
                'maximum': trail_aggregate[3],
                'median': trail_aggregate[4],
            }

            return {value: trail_aggregate[value] for value in self.tracked_values}
        except:
            logging.exception('Could not get data_values for SLA of KPI {}, due to:'.format(self.kpi_obj.n))
            return {value: 0 for value in self.tracked_values}


class ProtCodesKPISLAProcessor(KPISLAProcessor):
    @memoized_property
    def data_values(self):
        try:
            data_values = {}
            aggregator = ProtoCodesAggregator(self.latest_job, self.kpi_obj.params['case'])
            data = dict(aggregator.get_raw_data())
            if self.param_type == 'percent':
                data = dict(zip(list(data.keys()), ProtoCodesAggregator.count_percentage(list(data.values()))))
            for code in self.tracked_values:
                if any(symbol in ('x', 'х') for symbol in str(code)):
                    re_mask = str(code).replace('x', '[0-9]').replace('х', '[0-9]')
                    data_values[code] = sum(data[c] for c in list(data.keys()) if re.match(re_mask, str(c)))
                else:
                    data_values[code] = data.get(int(code), 0)
            return data_values
        except:
            logging.exception('Could not get data_values for SLA of KPI {}, due to:'.format(self.kpi_obj.n))
            return {code: 0 for code in self.tracked_values}


class NetCodesKPISLAProcessor(KPISLAProcessor):
    @memoized_property
    def data_values(self):
        try:
            data_values = {}
            aggregator = NetCodesAggregator(self.latest_job, self.kpi_obj.params['case'])
            data = dict(aggregator.get_raw_data())
            if self.param_type == 'percent':
                data = dict(zip(list(data.keys()), NetCodesAggregator.count_percentage(list(data.values()))))
            for code in self.tracked_values:
                if any(symbol in ('x', 'х') for symbol in str(code)):
                    # using non-greedy, because net codes are not necessarily 3-digit;
                    re_mask = str(code).replace('x', '[0-9]?').replace('х', '[0-9]?')
                    data_values[code] = sum([data[c] for c in list(data.keys()) if re.match(re_mask, str(c))])
                else:
                    data_values[code] = data.get(int(code), 0)
            return data_values
        except:
            logging.exception('Could not get data_values for SLA of KPI {}, due to:'.format(self.kpi_obj.n))
            return {code: 0 for code in self.tracked_values}


class RTHistogramsKPISLAProcessor(KPISLAProcessor):
    @memoized_property
    def data_values(self):
        try:
            data_values = {}
            aggregator = RTHistogramsAggregator(self.latest_job, self.kpi_obj.params['case'])
            data = OrderedDict(aggregator.get_raw_data())
            if self.param_type == 'percent':
                data = dict(zip(list(data.keys()), RTHistogramsAggregator.count_percentage(list(data.values()))))
                for code in self.tracked_values:
                    data_values[code] = data.get(int(code), 0)
            elif self.param_type == 'quantile':
                data = dict(zip(list(data.keys()), RTHistogramsAggregator.count_quantiles(list(data.values()))))
                for code in self.tracked_values:
                    try:
                        data_values[code] = data[int(code)]
                    except KeyError:
                        previous_time = max([t for t in list(data.keys()) if t <= int(code)] or [0])
                        data_values[code] = data.get(previous_time, 0)
            return data_values
        except:
            logging.exception('Could not get data_values for SLA of KPI {}, due to:'.format(self.kpi_obj.n))
            return {code: 0 for code in self.tracked_values}


class MonitoringKPISLAProcessor(KPISLAProcessor):
    @memoized_property
    def data_values(self):
        try:
            assert isinstance(self.kpi_obj.params, dict)
            aggregator = MonitoringAggregator(self.latest_job)
            monitoring_aggregate = aggregator.get_raw_data(self.kpi_obj.params['target'].n,
                                                           self.kpi_obj.params['metric'].id)
            assert monitoring_aggregate
            monitoring_aggregate = monitoring_aggregate[0]
            monitoring_aggregate = {
                'average': monitoring_aggregate[2],
                'stddev': monitoring_aggregate[3],
                'minimum': monitoring_aggregate[4],
                'maximum': monitoring_aggregate[5],
                'median': monitoring_aggregate[6],
            }
            return {value: monitoring_aggregate[value] for value in self.tracked_values}
        except AssertionError:
            return {value: 0 for value in self.tracked_values}
        except:
            logging.exception('Could not get data_values for SLA of KPI {}, due to:'.format(self.kpi_obj.n))
            return {value: 0 for value in self.tracked_values}


# ===============================================================================
#
# ===============================================================================
kpi_sla_processor_map = {
    'imbalance': ImbalanceKPISLAProcessor,
    'job_trail': QuantilesKPISLAProcessor,
    'trail': RTDetailsKPISLAProcessor,
    'trail_net': NetCodesKPISLAProcessor,
    'trail_resp': ProtCodesKPISLAProcessor,
    'trail_time': RTHistogramsKPISLAProcessor,
    'monitoring': MonitoringKPISLAProcessor,
}
