# Copyright (C) 2015-2018 Linaro Limited
#
# Author: Stevan Radakovic <stevan.radakovic@linaro.org>
#
# SPDX-License-Identifier: GPL-2.0-or-later

from decimal import Decimal
from json import dumps as json_dumps

from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import Group
from django.core import serializers
from django.core.exceptions import PermissionDenied
from django.db import IntegrityError
from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django_tables2 import RequestConfig

from lava_results_app.models import (
    Chart,
    ChartGroup,
    ChartQuery,
    ChartQueryUser,
    InvalidContentTypeError,
    Query,
    QueryCondition,
    QueryOmitResult,
    TestCase,
)
from lava_results_app.views import index
from lava_results_app.views.chart.decorators import ownership_required
from lava_results_app.views.chart.forms import (
    ChartForm,
    ChartQueryForm,
    ChartQueryUserForm,
)
from lava_results_app.views.chart.tables import (
    GroupChartTable,
    OtherChartTable,
    UserChartTable,
)
from lava_server.bread_crumbs import BreadCrumb, BreadCrumbTrail
from lava_server.lavatable import LavaView

CONDITIONS_SEPARATOR = ","
CONDITION_DIVIDER = "__"


class InvalidChartTypeError(Exception):
    """Raise when charting by URL has incorrect type argument."""


class TestCasePassFailChartError(Exception):
    """Raise when test case charts are of pass/fail type."""


class UserChartView(LavaView):
    def get_queryset(self):
        return Chart.objects.filter(owner=self.request.user).order_by("name")


class OtherChartView(LavaView):
    def get_queryset(self):
        # All published charts which are not part
        # of any group.
        other_charts = Chart.objects.filter(
            is_published=True, chart_group=None
        ).order_by("name")

        return other_charts


class GroupChartView(LavaView):
    def __init__(self, request, group, **kwargs):
        super().__init__(request, **kwargs)
        self.chart_group = group

    def get_queryset(self):
        # Specific group charts.
        group_charts = Chart.objects.filter(
            is_published=True, chart_group=self.chart_group
        ).order_by("name")

        return group_charts


def json_serializer(obj):
    """Convert objects that may appear in chart data to be JSON compatible."""
    if isinstance(obj, Decimal):
        return float(obj)
    raise TypeError("Type not serializable")


@BreadCrumb("Charts", parent=index)
def chart_list(request):
    group_tables = {}
    for group in ChartGroup.objects.all():
        if group.chart_set.count():
            prefix = "group_%s_" % group.id
            group_view = GroupChartView(
                request, group, model=Chart, table_class=GroupChartTable
            )
            table = GroupChartTable(
                group_view.get_table_data(prefix), request=request, prefix=prefix
            )
            group_tables[group.name] = table
            config = RequestConfig(request, paginate={"per_page": table.length})
            config.configure(table)

    prefix = "other_"
    other_view = OtherChartView(request, model=Chart, table_class=OtherChartTable)
    other_chart_table = OtherChartTable(
        other_view.get_table_data(prefix), request=request, prefix=prefix
    )
    config = RequestConfig(request, paginate={"per_page": other_chart_table.length})
    config.configure(other_chart_table)

    if request.user.is_authenticated:
        prefix = "user_"
        view = UserChartView(request, model=Chart, table_class=UserChartTable)
        user_chart_table = UserChartTable(
            view.get_table_data(prefix), request=request, prefix=prefix
        )
        config = RequestConfig(request, paginate={"per_page": user_chart_table.length})
        config.configure(user_chart_table)
    else:
        user_chart_table = None

    return render(
        request,
        "lava_results_app/chart_list.html",
        {
            "user_chart_table": user_chart_table,
            "other_chart_table": other_chart_table,
            "group_tables": group_tables,
            "bread_crumb_trail": BreadCrumbTrail.leading_to(chart_list),
            "context_help": ["lava-queries-charts"],
        },
    )


@BreadCrumb("Chart {name}", parent=chart_list, needs=["name"])
@login_required
def chart_display(request, name):
    chart = get_object_or_404(Chart, name=name)

    if not request.user.is_superuser:
        if not chart.is_published and chart.owner != request.user:
            raise PermissionDenied

    chart_data = {}
    for index, chart_query in enumerate(
        chart.chartquery_set.all().order_by("relative_index")
    ):
        chart_data[index] = chart_query.get_data(request.user)

    return render(
        request,
        "lava_results_app/chart_display.html",
        {
            "chart": chart,
            "chart_data": json_dumps(chart_data, default=json_serializer),
            "bread_crumb_trail": BreadCrumbTrail.leading_to(chart_display, name=name),
            "can_admin": chart.can_admin(request.user),
        },
    )


@BreadCrumb("Custom Chart", parent=chart_list)
@login_required
def chart_custom(request):
    content_type = Query.get_content_type(request.GET.get("entity"))

    chart_type = request.GET.get("type")
    chart_type_choices = ChartQuery._meta.get_field("chart_type").choices
    if not chart_type:
        chart_type = ChartQuery._meta.get_field("chart_type").default
    else:
        found = False
        for choice in chart_type_choices:
            if chart_type in choice:
                found = True
        if not found:
            raise InvalidChartTypeError(
                "Wrong chart type param. Please refer to chart docs."
            )

    if content_type.model_class() not in QueryCondition.RELATION_MAP:
        raise InvalidContentTypeError(
            "Wrong table name in entity param. Please refer to chart docs."
        )

    if content_type.model_class() == TestCase and chart_type == "pass/fail":
        raise TestCasePassFailChartError(
            "Chart of TestCase entity cannot be of pass/fail chart type."
        )

    conditions = Query.parse_conditions(content_type, request.GET.get("conditions"))

    chart = Chart(name="Custom")
    chart_query = ChartQuery(id=0)
    chart_query.chart = chart
    chart_query.chart_type = chart_type
    chart_data = {}
    chart_data[0] = chart_query.get_data(request.user, content_type, conditions)
    return render(
        request,
        "lava_results_app/chart_display.html",
        {
            "chart": chart,
            "chart_data": json_dumps(chart_data, default=json_serializer),
            "bread_crumb_trail": BreadCrumbTrail.leading_to(chart_custom),
            "can_admin": False,
        },
    )


@BreadCrumb("Chart {name}", parent=chart_list, needs=["name"])
@login_required
@ownership_required
def chart_detail(request, name):
    chart = get_object_or_404(Chart, name=name)
    return render(
        request,
        "lava_results_app/chart_detail.html",
        {
            "chart": chart,
            "chart_queries": chart.queries.all(),
            "bread_crumb_trail": BreadCrumbTrail.leading_to(chart_detail, name=name),
            "context_help": ["lava-queries-charts"],
        },
    )


@BreadCrumb("Add new", parent=chart_list)
@login_required
def chart_add(request):
    chart = Chart()
    chart.owner = request.user

    return chart_form(
        request,
        BreadCrumbTrail.leading_to(chart_add),
        instance=chart,
        query_id=request.GET.get("query_id"),
    )


@BreadCrumb("Edit {name}", parent=chart_detail, needs=["name"])
@login_required
@ownership_required
def chart_edit(request, name):
    chart = get_object_or_404(Chart, name=name)

    return chart_form(
        request, BreadCrumbTrail.leading_to(chart_edit, name=name), instance=chart
    )


@login_required
@ownership_required
def chart_delete(request, name):
    chart = get_object_or_404(Chart, name=name)

    chart.delete()
    return HttpResponseRedirect(reverse("lava.results.chart_list"))


@login_required
@ownership_required
def chart_toggle_published(request, name):
    chart = get_object_or_404(Chart, name=name)

    chart.is_published = not chart.is_published
    chart.save()

    return HttpResponseRedirect(reverse("lava.results.chart_detail", args=(name,)))


@login_required
def chart_group_list(request):
    term = request.GET["term"]
    groups = [
        str(group.name) for group in ChartGroup.objects.filter(name__istartswith=term)
    ]
    return JsonResponse(groups, safe=False)


@login_required
def chart_add_group(request, name):
    if request.method != "POST":
        raise PermissionDenied

    group_name = request.POST.get("value")
    chart = get_object_or_404(Chart, name=name)
    old_group = chart.chart_group

    if not group_name:
        chart.chart_group = None
    else:
        new_group = ChartGroup.objects.get_or_create(name=group_name)[0]
        chart.chart_group = new_group

    chart.save()

    if old_group:
        if not old_group.chart_set.count():
            old_group.delete()

    return HttpResponseRedirect(reverse("lava.results.chart_detail", args=(name,)))


@login_required
@ownership_required
def chart_select_group(request, name):
    if request.method != "POST":
        raise PermissionDenied

    group_name = request.POST.get("value")
    chart = get_object_or_404(Chart, name=name)

    if not group_name:
        chart.group = None
    else:
        try:
            group = Group.objects.get(name=group_name)
        except Group.DoesNotExist:
            group = None
        chart.group = group

    chart.save()

    return HttpResponseRedirect(reverse("lava.results.chart_detail", args=(name,)))


@login_required
def get_chart_group_names(request):
    term = request.GET["term"]
    groups = []
    for group in Group.objects.filter(user=request.user, name__istartswith=term):
        groups.append({"id": group.id, "name": group.name, "label": group.name})
    return JsonResponse(groups, safe=False)


@BreadCrumb("Add chart query", parent=chart_detail, needs=["name"])
@login_required
@ownership_required
def chart_query_add(request, name):
    chart = get_object_or_404(Chart, name=name)

    return chart_query_form(
        request, BreadCrumbTrail.leading_to(chart_query_add, name=name), chart=chart
    )


@BreadCrumb("Edit chart query", parent=chart_detail, needs=["name", "id"])
@login_required
@ownership_required
def chart_query_edit(request, name, id):
    chart_query = get_object_or_404(ChartQuery, id=id)

    return chart_query_form(
        request,
        BreadCrumbTrail.leading_to(chart_query_edit, name=name, id=id),
        instance=chart_query,
    )


@login_required
@ownership_required
def chart_query_remove(request, name, id):
    chart_query = get_object_or_404(ChartQuery, id=id)
    chart_query.delete()

    return HttpResponseRedirect(reverse("lava.results.chart_detail", args=(name,)))


@login_required
@ownership_required
def chart_omit_result(request, name, id, result_id):
    chart_query = get_object_or_404(ChartQuery, id=id)
    result_object = get_object_or_404(
        chart_query.query.content_type.model_class(), id=result_id
    )

    try:
        QueryOmitResult.objects.create(
            query=chart_query.query, content_object=result_object
        )
    except IntegrityError:
        # Ignore unique constraint violation.
        pass

    return HttpResponseRedirect(chart_query.chart.get_absolute_url())


@login_required
def chart_query_order_update(request, name):
    if request.method != "POST":
        raise PermissionDenied

    chart_query_order = request.POST.get("chart_query_order").split(",")

    try:
        for index, chart_query_id in enumerate(chart_query_order):
            chart_query = ChartQuery.objects.get(pk=chart_query_id)
            chart_query.relative_index = index
            chart_query.save()
    except Exception:
        return JsonResponse("fail", safe=False)

    return JsonResponse("success", safe=False)


@login_required
def settings_update(request, name, id):
    if request.method != "POST":
        raise PermissionDenied

    try:
        instance = ChartQueryUser.objects.get(user=request.user, chart_query__id=id)
    except ChartQueryUser.DoesNotExist:
        # Create new.
        instance = ChartQueryUser()
        instance.chart_query_id = id
        instance.user = request.user

    form = ChartQueryUserForm(request.user, request.POST, instance=instance)
    if form.is_valid():
        instance = form.save()
        data = serializers.serialize("python", [instance])
        return JsonResponse(data, safe=False)
    else:
        return HttpResponseBadRequest()


def chart_form(request, bread_crumb_trail, instance=None, query_id=None):
    if request.method == "POST":
        form = ChartForm(request.user, request.POST, instance=instance)
        if form.is_valid():
            chart = form.save()
            if query_id:
                query = Query.objects.get(pk=query_id)
                chart_query = ChartQuery(chart=chart, query=query)
                chart_query.save()

            return HttpResponseRedirect(
                reverse("lava.results.chart_detail", args=(chart.name,))
            )

    else:
        form = ChartForm(request.user, instance=instance)
        form.fields["owner"].initial = request.user

    return render(
        request,
        "lava_results_app/chart_form.html",
        {"bread_crumb_trail": bread_crumb_trail, "form": form},
    )


def chart_query_form(request, bread_crumb_trail, chart=None, instance=None):
    if request.method == "POST":
        form = ChartQueryForm(request.user, request.POST, instance=instance)
        if form.is_valid():
            chart_query = form.save()
            return HttpResponseRedirect(
                reverse("lava.results.chart_detail", args=(chart_query.chart.name,))
            )

    else:
        form = ChartQueryForm(request.user, instance=instance)
        form.fields["chart"].initial = chart

    return render(
        request,
        "lava_results_app/chart_query_form.html",
        {
            "bread_crumb_trail": bread_crumb_trail,
            "form": form,
            "instance": instance,
        },
    )
