import inspect
from math import floor, log10

from common.util import ClickhouseClient


def filter_type_strings(text):
    return '\n'.join([line for line in text.split('\n') if ':type' not in line]) if text is not None else ''


def get_functions_spec():
    return [
        {'name': name,
         'description': filter_type_strings(inspect.getdoc(func)),
         'parametrized': len(inspect.signature(func).parameters) > 1} for name, func
        in inspect.getmembers(StatFunctions,
                              predicate=lambda o: (inspect.ismethod(o) or inspect.isfunction(o)))
        if not name.startswith('_')
    ]


def get_rounder(n):
    """
    :param n: number of significant figures to round to
    :type n: int
    :return: function of float that rounds that float to n figures
    :rtype: callable
    """
    return lambda x: round(x, -int(floor(log10(abs(x)))) + (n - 1)) if x != 0 else 0


class StatFunctions(object):
    cc = ClickhouseClient()
    _rounder3 = get_rounder(3)

    @staticmethod
    def min(case_object):
        """
        absolute minimum of a metric
        :type case_object: volta.common.models.Case
        """
        return get_min_max_avg(case_object, 'min')

    @staticmethod
    def max(case_object):
        """
        absolute maximum of a metric
        :type case_object: volta.common.models.Case
        """
        return get_min_max_avg(case_object, 'max')

    @staticmethod
    def avg(case_object):
        """
        sum of all values divided by their amount
        :type case_object: volta.common.models.Case
        """
        return get_min_max_avg(case_object, 'avg')

    @classmethod
    def percent(cls, case_object, *event):
        """
        :type case_object: volta.common.models.Case
        :rtype: int
        """
        assert case_object.data.has_histograms, 'percent function is only defined for histograms data type'
        return cls._rounder3(cc_get_fraction_from_hist(case_object.tag.hex, *event) * 100)

    @staticmethod
    def q_cum(case_object, quantile_percent):
        """

        :type case_object: volta.common.models.Case
        """
        assert quantile_percent.isdigit(), "'q_cum' argument should be integer"
        quantile_percent = int(quantile_percent)
        ok_quantile_precents = [0, 10, 25, 50, 75, 80, 85, 90, 95, 98, 99, 100]
        assert quantile_percent in ok_quantile_precents, \
            "'q_cum' arguments should be in {}".format(ok_quantile_precents)
        assert case_object.data.has_distributions, "Filtered metric should have distributions to apply 'q_cum'"
        if quantile_percent == 0:
            str_quantile = 'min'
        elif quantile_percent == 100:
            str_quantile = 'max'
        else:
            str_quantile = 'q{}'.format(quantile_percent)
        result = case_object.summary(fillna=0)[str_quantile][0]
        try:
            return float(result)
        except ValueError:
            raise ValueError(
                'Function q_cum returned {} for job {}, expected float'.format(result, case_object.data.job.id)
            )

    @staticmethod
    def get_autostop_rps(case_object):
        return StatFunctions.get_meta_key(case_object, 'autostop_rps', 0)

    @staticmethod
    def get_meta_key(case_object, meta_key, default=None):
        """
        Returns value of an integer meta parameter, associated with job
        :type case_object: volta.common.models.Case
        :param meta_key: str
        """
        meta = case_object.data.job.meta
        job_id = case_object.data.job.id
        if meta_key not in meta.keys():
            if default is not None:
                return default
            else:
                raise ValueError('{} is not present in job {}'.format(meta_key, job_id))
        result = meta[meta_key]
        if not result.isdigit():
            raise ValueError('Meta value with key {} in job {} is not digital'.format(meta_key, job_id))
        return int(result)

    @staticmethod
    def total(case_object, *value):
        """
        :type case_object: volta.common.models.Case
        :return: int
        """
        return cc_get_total_events(case_object.tag.hex, *value)

    @classmethod
    def median(cls, case_object):
        """
        :type case_object: volta.common.models.Case
        :return: float
        """
        if case_object.data.has_histograms:
            query = '''
            SELECT median(s) FROM (SELECT sum(cnt) s FROM histograms WHERE tag='{tag}' GROUP BY ts)
            '''
        elif case_object.data.has_raw:
            query = "SELECT median(value) FROM metrics WHERE tag='{tag}'"
        elif case_object.data.has_aggregates:
            return case_object.summary(fillna=None)['q50'][0]
        else:
            raise ValueError("Can not calculate median value of case {} of data {}".format(case_object.name,
                                                                                           case_object.data.id))
        return cls.cc.select(query, {'tag': case_object.tag.hex})[0][0]

    @classmethod
    def percent_rank(cls, case_object, value):
        """
        :type case_object: volta.common.models.Case
        :return: float
        """
        if case_object.data.has_raw:
            query = '''
            SELECT countIf(value<{value})/count() FROM metrics WHERE tag='{tag}'
            '''
        elif case_object.data.has_distributions:
            query = '''
            SELECT sumIf(cnt, r<{value})/sum(cnt) FROM distributions WHERE tag='{tag}'
            '''
        elif case_object.data.has_histograms:
            query = '''
            SELECT sumIf(cnt, category='{value}')/sum(cnt) from histograms where tag='{tag}'
            '''
        elif case_object.data.has_events:
            query = '''
            SELECT countIf(value='{value}')/count() FROM events Where tag='{tag}'
            '''
        else:
            raise ValueError('Can not calculate percent_rank of case {} of data {}'.format(case_object.name,
                                                                                           case_object.data.id))
        return cls._rounder3(
            cls.cc.select(query, {'tag': case_object.tag.hex, 'value': value})[0][0] * 100)

    @classmethod
    def stddev(cls, case):
        """
        :type case: volta.common.models.Case
        :rtype: float
        """
        if case.data.has_raw:
            query = "SELECT stddevPop(value) from metrics where tag='{tag}'"
        elif case.data.has_aggregates:
            query = "SELECT stddevPop(average) from aggregates where tag='{tag}'"
        else:
            raise ValueError("Can not calculate stddev of case {} of metric".format(case.name,
                                                                                    case.data.id))
        return cls._rounder3(cls.cc.select(query, {'tag': case.tag.hex})[0][0])


def get_min_max_avg(case_object, method):
    """

    :type case_object: volta.common.models.Case
    """
    if case_object.data.has_aggregates:
        return cc_get_from_aggr(case_object.tag.hex, method)
    elif case_object.data.has_raw:
        return cc_get_from_raw(case_object.tag.hex, method)
    elif case_object.data.has_distributions:
        return cc_get_from_distr(case_object.tag.hex, method)
    else:
        raise ValueError('Cant find min for metric {}. Metric has {}'.format(case_object.tag.hex,
                                                                             case_object.data.types_list()))


def cc_get_from_raw(tag, method):
    query = '''
        SELECT {method}(value) FROM metrics WHERE tag='{tag}'
        '''
    cc_data = ClickhouseClient().select(query, query_params={'tag': tag,
                                                             'method': method})
    return cc_data[0][0]


def cc_get_from_aggr(tag, method):
    method = {
        'min': 'min(q0)',
        'max': 'max(q100)',
        'avg': 'sum(sum)/sum(cnt)'
    }[method]
    query = '''
    SELECT {method} FROM aggregates WHERE tag='{tag}'
    '''
    cc_data = ClickhouseClient().select(query, query_params={'tag': tag,
                                                             'method': method})
    return cc_data[0][0]


def cc_get_from_distr(tag, method):
    query = '''
    SELECT {method}(r) FROM distributions WHERE tag='{tag}' AND cnt > 0
    '''
    cc_data = ClickhouseClient().select(query, query_params={'tag': tag,
                                                             'method': method})
    return cc_data[0][0]


def cc_get_fraction_from_hist(tag, *event):
    patterns = {'ptn{}'.format(n): ptn for n, ptn in enumerate(event)}
    query = '''
    WITH (SELECT sum(cnt) FROM histograms where tag='{{tag}}') as total
    SELECT if(total > 0, sum(cnt)/total, 0)
    FROM histograms
    WHERE tag='{{tag}}' AND multiMatchAny(category, [{patterns}])
    '''.format(patterns=', '.join(["'{" + ptn_name + "}'" for ptn_name in patterns]))
    query_params = dict({'tag': tag}, **patterns)
    cc_data = ClickhouseClient().select(query, query_params=query_params)
    return cc_data[0][0]


def cc_get_total_events(tag, *value):
    patterns = {'ptn{}'.format(n): ptn for n, ptn in enumerate(value)}
    query = '''
    SELECT sum(cnt) FROM histograms WHERE tag='{{tag}}' and multiMatchAny(category, [{patterns}])
    '''.format(patterns=', '.join(["'{" + ptn_name + "}'" for ptn_name in patterns]))
    query_params = dict({'tag': tag}, **patterns)
    cc_data = ClickhouseClient().select(query, query_params=query_params)
    return int(cc_data[0][0])

