import json
import logging
import sys
import warnings
import datetime
from collections import defaultdict

import pandas as pd
import yaml
from django.db import IntegrityError
from django.db.models import F
from django.http import HttpResponseBadRequest, HttpResponseNotAllowed, HttpResponse, HttpResponseNotFound
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.base import View
from django.utils.decorators import method_decorator

from common.layout import common_layout
from common.models import Regression, Job, RegressionMeta
from common.util import escape_string
from common.widgets import get_regression_sections
from regression.util.stat_functions import get_functions_spec
from .layouts import REGRESSION_LIST_LAYOUT
from .services import _regression_meta_layout, _select_regression_slice, _filter_by_attributes, _filter_by_meta, \
    _parse_filters, _validate_creation, delete_regression_info, create_regression_info, check_jobs, check_filters,\
    get_sla_results

if not sys.warnoptions:
    warnings.simplefilter("ignore")


@csrf_exempt
def create_regression(request):
    if request.method != 'POST':
        return HttpResponseNotAllowed(['POST'])
    try:
        body = yaml.safe_load(request.body)
        assert isinstance(body, dict)
    except yaml.YAMLError as yaml_exc:
        return HttpResponseBadRequest(str(yaml_exc))
    except AssertionError:
        return HttpResponseBadRequest('Body is not a dict: {}'.format(request.body))
    try:
        person, config = body['person'], body['config']
        assert isinstance(person, str), 'Person should be string'
        assert isinstance(config, dict), 'Config should be dict'
    except KeyError:
        return HttpResponseBadRequest('person and config are required fields')
    except AssertionError as aexc:
        return HttpResponseBadRequest(str(aexc))
    normalized_config, errors = _validate_creation(config)
    if errors:
        return HttpResponseBadRequest(json.dumps(errors), status=422)
    else:
        series_list = config.get('series_list', [])
        existing_jobs, missing_jobs = check_jobs(config.get('test_ids', []))
        filter_errors = check_filters(existing_jobs, series_list)
        if len(filter_errors) > 0:
            return HttpResponseBadRequest('\n'.join(filter_errors), status=409)
        try:
            regression = Regression.objects.create(name=config['name'],
                                                   person=person,
                                                   creation=int(datetime.datetime.now().timestamp() * 1000000))
        except IntegrityError:
            return HttpResponseBadRequest('Regression {} already exists'.format(config['name']))
        else:
            if existing_jobs:
                regression.jobs.add(*existing_jobs)
        create_regression_info(regression, config)
        return HttpResponse(json.dumps({
            'regression': regression.name,
            'errors': {'invalid_test_ids': missing_jobs} if missing_jobs else {}
        }), content_type='application/json; charset=utf-8')


@method_decorator(csrf_exempt, name='dispatch')
class RegressionPage(View):
    def get(self, request, regr_name):
        try:
            regression = Regression.objects.get(name=regr_name)
            series_list = regression.regressionseries_set.all()
        except Regression.DoesNotExist:
            return HttpResponseBadRequest("Regression with name {} doesn't exist".format(regr_name))
        return HttpResponse(
            json.dumps({
                'name': regression.name,
                'test_ids': [test.id for test in regression.jobs.all()],
                'meta': regression.regression_meta,
                'series_list': [
                    {'filter': seria.filter,
                     'sla': [sla.dict_repr for sla in seria.sla_set.all()]}
                    for seria in series_list
                ]
            }),
            content_type='application/json; charset=utf-8'
        )

    def put(self, request, regr_name):
        try:
            config = yaml.safe_load(request.body)
            assert isinstance(config, dict)
            regression = Regression.objects.get(name=regr_name)
        except yaml.YAMLError as yaml_exc:
            return HttpResponseBadRequest(str(yaml_exc))
        except AssertionError:
            return HttpResponseBadRequest('Config is not a dict: {}'.format(request.body))
        except Regression.DoesNotExist:
            return HttpResponseBadRequest("Regression with name {} doesn't exist".format(regr_name))

        config, errors = _validate_creation(config, regr_name)
        if errors:
            return HttpResponseBadRequest(json.dumps(errors), status=422)

        name = config['name']
        if name != regr_name:
            regression.name = name
            regression.save()

        delete_regression_info(regression)
        create_regression_info(regression, config)

        return HttpResponse(
            json.dumps({
                'regression': regression.name
            }),
            content_type='application/json; charset=utf-8'
        )


@csrf_exempt
def add_job_to_regression(request, regr_name):
    if request.method != 'POST':
        return HttpResponseNotAllowed(['POST'])
    try:
        job_ids = json.loads(request.body.decode('utf8'))
        assert isinstance(job_ids, list), 'Request body should contain a list'
        assert all(map(lambda job: isinstance(job, int), job_ids)), "'job' should be integers"
        assert len(job_ids), "'job' shouldn't be empty"

        regression = Regression.objects.get(name=regr_name)
    except ValueError:
        return HttpResponseBadRequest('Request body should contain an integer list')
    except AssertionError as aexc:
        return HttpResponseBadRequest(repr(aexc))
    except Regression.DoesNotExist:
        return HttpResponseBadRequest("Regression with name {} doesn't exist".format(regr_name))
    except TypeError as e:
        return HttpResponseBadRequest(e)

    errors = []
    added_tests = []
    existing_jobs = regression.jobs.all()
    for job_id in job_ids:
        try:
            job = Job.objects.get(id=job_id)
            assert job not in existing_jobs
            regression.jobs.add(job)
            added_tests.append(job_id)
        except Job.DoesNotExist:
            errors.append("Job with id {} doesn't exist".format(job_id))
        except AssertionError:
            errors.append('Job with id {} already presented in regression'.format(job_id))
    return HttpResponse(
        json.dumps({
            'added_tests': added_tests,
            'errors': errors
        }),
        content_type='application/json; charset=utf-8'
    )


@csrf_exempt
def delete_job_from_regression(request, regr_name):
    if request.method != 'POST':
        return HttpResponseNotAllowed(['POST'])
    try:
        job_ids = json.loads(request.body.decode('utf8'))
        assert isinstance(job_ids, list), 'Request body should contain a list'
        assert all(map(lambda job: isinstance(job, int), job_ids)), "'job' should be integers"
        assert len(job_ids), "'job' shouldn't be empty"

        regression = Regression.objects.get(name=regr_name)
    except ValueError:
        return HttpResponseBadRequest('Request body should contain an integer list')
    except AssertionError as aexc:
        return HttpResponseBadRequest(repr(aexc))
    except Regression.DoesNotExist:
        return HttpResponseBadRequest("Regression with name {} doesn't exist".format(regr_name))

    errors = []
    deleted_tests = []
    existing_jobs = regression.jobs.all()
    for job_id in job_ids:
        try:
            job = Job.objects.get(id=job_id)
            assert job in existing_jobs
            regression.jobs.remove(job)
            deleted_tests.append(job_id)
        except Job.DoesNotExist:
            errors.append("Job with id {} doesn't exist".format(job_id))
        except AssertionError:
            errors.append("Job with id {} isn't presented in regression".format(job_id))
    return HttpResponse(
        json.dumps({
            'deleted_tests': deleted_tests,
            'errors': errors
        }),
        content_type='application/json; charset=utf-8'
    )


def regression_list_layout(request):
    return HttpResponse(
        json.dumps(REGRESSION_LIST_LAYOUT),
        content_type='application/json; charset=utf-8'
    )


def regression_list(request):
    """
    параметры в запросе
    - filter - набор фильтров, например filter=person:noob|direvius то есть person = или noob, или direvius
    - fields - набор требуемых полей
    - order - по какому аттрибуту сортировать. используем джанговый синтаксис для направления:
        id = asc, -id = desc
    - after - id последней стрельбы, которая уже есть на фронте
    - count - количество запрашиваемых стрельб

    :param request: django HTTP request
    :return: django HTTP response 200 со списком информации по регрессиям согласно запрошенным фильтрам и пр. Или 400.
    """

    filter_attributes = request.GET.getlist('filter_attribute')
    filter_metas = request.GET.getlist('filter_meta')
    order = request.GET.get('order', '-id')
    fields = request.GET.getlist('field')
    count = request.GET.get('count', '20')
    after = request.GET.get('after', '0')

    try:
        assert count.isdigit() and after.isdigit()
        count = int(count)
        after = int(after)
    except AssertionError:
        return HttpResponseBadRequest('"count" and "after" should be digital')

    try:
        meta_filters = _parse_filters(filter_metas)
        attributes_filters = _parse_filters(filter_attributes)
    except ValueError:
        return HttpResponseBadRequest('"filter" should be in form key:value1|value2|value3')
    except AssertionError as aexc:
        return HttpResponseBadRequest(repr(aexc))

    try:
        queryset = _filter_by_meta(
            _filter_by_attributes(
                Regression.objects.all(), attributes_filters),
            meta_filters
        )
    except ValueError as vexc:
        return HttpResponseBadRequest(repr(vexc))
    else:
        if order.startswith('-'):
            order = order.strip('-')
            queryset = queryset.order_by(F(order).desc(nulls_last=True))
        else:
            queryset = queryset.order_by(F(order).asc(nulls_last=True))

    try:
        regressions, no_more = _select_regression_slice(queryset, after, count)
    except ValueError:
        return HttpResponseBadRequest('Regression {} is not present in filtered regressions list'.format(after))

    regressions_meta = [r.attributes_dict(keys=fields) for r in regressions]

    return HttpResponse(json.dumps({
        'regressions': regressions_meta,
        'no_more': no_more
    }), content_type='application/json; charset=utf-8')


def regression_meta_keys(request):
    meta_keys = [rm.key for rm in RegressionMeta.objects.distinct('key')]
    return HttpResponse(
        json.dumps(meta_keys),
        content_type='application/json; charset=utf-8'
    )


def get_regression_meta(request):
    regressions = request.GET.getlist('regression')
    regressions = Regression.objects.filter(name__in=regressions)
    result = {regression.id: regression.regression_meta for regression in regressions}
    return HttpResponse(json.dumps(result), content_type='application/json; charset=utf-8')


def get_metrics_meta(request):
    result = defaultdict(dict)
    try:
        jobs = request.GET.getlist('job')
        assert jobs and all([job.isdigit() for job in jobs]), 'invalid jobs {}'.format(jobs)
    except AssertionError as exc:
        return HttpResponseBadRequest(repr(exc))

    for job in jobs:
        result.update({str(job): {}})

    result = {str(job.id): {
        data.id: data.meta for data in job.data_set.all()
    }
        for job in Job.objects.filter(id__in=jobs).prefetch_related('data_set__datameta_set')
    }

    return HttpResponse(json.dumps(result), content_type='application/json; charset=utf-8')


def get_functions(request):
    return HttpResponse(json.dumps(get_functions_spec()), content_type='application/json; charset=utf-8')


def get_layout(request):
    """
    params:
    - regression_name

    :param request: django HTTP request
    :return: django HTTP response
    """
    try:
        regression_name = request.GET.get('regression')
        assert regression_name
        regression = Regression.objects.get(name=regression_name)
    except AssertionError:
        return HttpResponseBadRequest('Parameter "regression" is required')
    except Regression.DoesNotExist:
        # noinspection PyUnboundLocalVariable
        return HttpResponseNotFound('Regression {} not found'.format(regression_name))

    layout = common_layout(
        get_regression_sections(regression),
        _regression_meta_layout(regression),
        regression.attributes_dict())
    return HttpResponse(
        json.dumps(layout), content_type='application/json; charset=utf-8'
    )


def regression_data(request):
    try:
        sla_ids = [int(escape_string(id_)) for id_ in request.GET.getlist('sla_id')]
    except ValueError:
        return HttpResponseBadRequest(json.dumps(
            {'errors': ['Sla ids should be integers: {}'.format(', '.join(request.GET.getlist('sla_id')))]}))
    if not sla_ids:
        return HttpResponseBadRequest(json.dumps({'errors': ['No sla ids given']}))

    results_df = pd.DataFrame()
    errors = []
    for sla_id in sla_ids:
        res, errs = get_sla_results(sla_id)
        results_df = pd.concat([results_df,
                                pd.DataFrame({'{}_y'.format(sla_id): [value for key, value in res]},
                                             index=[key for key, value in res])],
                               axis=1)
        errors.extend(errs)
    if results_df.empty:
        result = ''
    else:
        results_df.index.name = 'x'
        result = results_df.to_csv()
    if errors:
        logging.warning('\n\t'.join(errors))
    return HttpResponse(json.dumps({'result': result, 'errors': errors}),
                        content_type='application/json; charset=utf-8')
