import copy
import inspect
import logging
import threading
import time
from collections import defaultdict
from cerberus import Validator
from django.db.models import Q
from common.models import RegressionMeta, RegressionSeries, SLA, Job, StatStorage, Case
from regression.request_schemes import get_config_schema
from regression.util.stat_functions import StatFunctions


def _regression_meta_layout(regression):
    """
    :type regression: common.models.Regression
    """
    return {
        regression.name: [
            {
                "isMutable": False,
                "label": "Regression Name",
                "locations": ["info"],
                "dataStore": "attributes",
                "dataSource": "name",
                "type": "String"
            },
            {
                "isMutable": False,
                "label": "Person",
                "locations": ["info"],
                "dataStore": "attributes",
                "dataSource": "person",
                "type": "Staff"
            },
            {
                "isMutable": False,
                "label": "Last Test",
                "locations": ["info"],
                "dataStore": "attributes",
                "dataSource": "last_job",
                "type": "IDLink"
            }
        ]
    }


def _select_regression_slice(queryset, after, count):
    ids = [regression.id for regression in queryset]
    start_id = ids.index(after) + 1 if after else 0
    no_more = start_id + count >= len(queryset)
    return queryset[start_id:start_id + count], no_more


def _filter_by_attributes(queryset, filters):
    for key, possible_values in filters.items():
        any_filter = Q()
        for value in possible_values:
            kwargs = {'{}__contains'.format(key): value}
            any_filter = any_filter | Q(**kwargs)
        queryset = queryset.filter(any_filter)
    return queryset


def _filter_by_meta(queryset, filters):
    for key, possible_values in filters.items():
        queryset = queryset.filter(regressionmeta__key=key)
        any_filter = Q()
        for value in possible_values:
            any_filter = any_filter | Q(regressionmeta__value__contains=value)
        queryset = queryset.filter(any_filter)
    return queryset


def _parse_filters(filters):
    meta_filters = {}
    for filter_ in filters:
        filter_key, values_string = filter_.split(':', 1)
        filter_values = values_string.split('|')
        meta_filters[filter_key] = filter_values
    return meta_filters


def _validate_creation(settings, current_name=None):
    v = Validator(get_config_schema(current_name=current_name))
    v.validate(settings)
    errors = _prettify_errors(v.errors)
    return v.document, errors


def _prettify_errors(error_struct, path=''):
    """
    Приводит формат ошибок cerberus к виду {error.path: error_value, error.path[2]: error_value2}
    Не важно как
    """
    result = defaultdict(list)
    for key, error_list in error_struct.items():
        if isinstance(key, int):
            formatted_key = '[{}]'.format(key)
        else:
            formatted_key = '.{}'.format(key) if path else key
        inner_path = '{}{}'.format(path, formatted_key)
        for error in error_list:
            if isinstance(error, str):
                result[inner_path].append(error)
            else:
                inner_errors = _prettify_errors(error, inner_path)
                result.update(inner_errors)
    return result


def delete_regression_info(regression):
    RegressionMeta.objects.filter(regression=regression).delete()
    RegressionSeries.objects.filter(regression=regression).delete()


def create_regression_info(regression, config):
    meta = config.get('meta', {})
    series_list = config.get('series_list', [])
    for regression_seria in series_list:
        series = RegressionSeries.objects.create(regression=regression,
                                                 filter=regression_seria['filter'])
        slas = SLA.objects.bulk_create(
            [SLA.from_cfg(series, **args) for args in regression_seria['sla']]
        )
    regr_meta = RegressionMeta.objects.bulk_create(
        [RegressionMeta(regression=regression, key=k, value=v) for k, v in meta.items()]
    )


def check_jobs(job_ids):
    valid = Job.objects.filter(pk__in=job_ids).values_list('id', flat=True)
    invalid = list(set(job_ids) - set(valid))
    return valid, invalid


def check_filters(job_ids, series_list):
    errors = []
    for job_id in job_ids:
        for seria in series_list:
            _filter, sla = seria['filter'], seria['sla']
            metrics = filter_job_metrics(_filter, job_id)
            if len(metrics) > 1:
                errors.append('Filter {} returned more than one metric for test {}:\n'
                              '{}'.format(_filter, job_id, list([m.id for m in metrics])))
    return errors


def compute_regression_value(job, function_name, args, _filter):
    """
    :rtype: (float, str)
    """
    case_objects = filter_job_metrics(_filter, job.pk)
    if len(case_objects) > 0:
        data_object = case_objects[0]
        function = inspect.getmembers(StatFunctions, (lambda a: callable(a) and a.__name__ == function_name))[0][1]
        try:
            return function(data_object, *args), None
        except (ValueError, AssertionError) as e:
            logging.warning('Function {} value {} not expected or series data type not supported'.format(function_name, args), exc_info=True)
            return None, str(e.args)
    else:
        return None, None


def compute_regression_values(sla, missing_jobs):
    """
    :type missing_jobs: set of Job
    :type sla: common.models.SLA
    """
    stat_storage = []
    errors = []
    for job in missing_jobs:
        computed_value, error = compute_regression_value(job, sla.function, sla.args, sla.regression_series.filter)
        if computed_value is not None:
            stat_storage.append(StatStorage(job=job,
                                            function=sla.function,
                                            filter=sla.regression_series.filter,
                                            args=sla.args,
                                            value=computed_value))
        if error:
            errors.append(error)
    return StatStorage.objects.bulk_create(stat_storage), errors


def filter_job_metrics(fltr: dict, job_id: int) -> list:
    """
    returns only metrics that match filter
    :param fltr: filter for values of DataMeta and 'case'
    :return: Data objects of specified job that match all the filters
    """
    CASE_KEY = 'case'
    # have to do this to save original filter in StatsStorage as we dont have 'case' field there
    _fltr = copy.deepcopy(fltr)
    case_name = _fltr.pop(CASE_KEY, Case.OVERALL)
    queryset = Case.objects.filter(data__job_id=job_id, name=case_name)
    for k, v in _fltr.items():
        queryset = queryset.filter(data__datameta__key=k, data__datameta__value=v)
    return queryset


def get_sla_results(sla_id):
    start = time.time()
    try:
        sla = SLA.objects.get(pk=sla_id)
    except SLA.DoesNotExist:
        return [], ['SLA id {} does not exist'.format(sla_id)]

    jobs = sla.regression_series.regression.jobs.all()
    data = StatStorage.objects.filter(job__in=jobs,
                                      filter=sla.regression_series.filter,
                                      function=sla.function,
                                      args=sla.args
                                      ).order_by('job_id')
    logging.debug("data prepare time: {}".format(time.time()-start))
    start = time.time()

    existing_data = {stat.job.pk: stat.value for stat in data.all()}
    missing_jobs = set(jobs).difference(set(d.job for d in data))
    logging.debug("data separation time: {}".format(time.time() - start))
    start = time.time()

    stats, errors = compute_regression_values(sla, missing_jobs)
    added_data = {stat.job.pk: stat.value for stat in stats}
    logging.debug("data computation time: {}".format(time.time() - start))

    all_sla_data = sorted({**existing_data, **added_data}.items())
    if not all_sla_data:
        errors.append('No data for sla "{}" in regression {} sla_id: {}'.format(sla.name,
                                                                                sla.regression_series.regression.name,
                                                                                sla_id))
    return all_sla_data, errors


def compute_new_test_in_bg(job_obj: Job):
    slas = [sla for regr in job_obj.regression_set.all() for sla in regr.get_slas()]
    threading.Thread(target=lambda: [compute_regression_values(sla, {job_obj}) for sla in slas]).start()
