# Copyright (C) 2018 Linaro Limited
#
# Author: Neil Williams <neil.williams@linaro.org>
#         Remi Duraffort <remi.duraffort@linaro.org>
#
# SPDX-License-Identifier: GPL-2.0-or-later

import warnings

from django import forms
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.admin import GroupAdmin, UserAdmin
from django.contrib.auth.models import Group, Permission, User
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Prefetch, Q

from lava_scheduler_app.models import (
    Alias,
    Architecture,
    BitWidth,
    Core,
    Device,
    DeviceType,
    GroupDevicePermission,
    GroupDeviceTypePermission,
    GroupWorkerPermission,
    JobFailureTag,
    NotificationRecipient,
    ProcessorFamily,
    RemoteArtifactsAuth,
    Tag,
    TestJob,
    Worker,
)
from linaro_django_xmlrpc.models import AuthToken

# django admin API itself isn't pylint clean, so some settings must be suppressed.


class GroupObjectPermissionInline(admin.TabularInline):
    extra = 0
    supported_permissions = (
        DeviceType.PERMISSIONS_PRIORITY
        + Device.PERMISSIONS_PRIORITY
        + Worker.PERMISSIONS_PRIORITY
    )

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
        supported_codenames = [x.split(".")[1] for x in self.supported_permissions]
        if db_field.name == "permission":
            kwargs["queryset"] = Permission.objects.filter(
                content_type__model=self.parent_model._meta.object_name.lower(),
                codename__in=supported_codenames,
            )
        return super().formfield_for_foreignkey(db_field, request, **kwargs)


class GroupDeviceTypePermissionInline(GroupObjectPermissionInline):
    model = GroupDeviceTypePermission
    extra = 0


class GroupDevicePermissionInline(GroupObjectPermissionInline):
    model = GroupDevicePermission
    extra = 0


class GroupWorkerPermissionInline(GroupObjectPermissionInline):
    model = GroupWorkerPermission
    extra = 0


@admin.register(Alias)
class AliasAdmin(admin.ModelAdmin):
    def get_readonly_fields(self, _, obj=None):
        if obj:  # editing an existing object
            return self.readonly_fields + ("name",)
        return self.readonly_fields

    list_display = ("name", "device_type")


@admin.register(Architecture)
class ArchitectureAdmin(admin.ModelAdmin):
    def get_readonly_fields(self, _, obj=None):
        if obj:  # editing an existing object
            return self.readonly_fields + ("name",)
        return self.readonly_fields


@admin.register(BitWidth)
class BitWidthAdmin(admin.ModelAdmin):
    def get_readonly_fields(self, _, obj=None):
        if obj:  # editing an existing object
            return self.readonly_fields + ("width",)
        return self.readonly_fields


@admin.register(Core)
class CoreAdmin(admin.ModelAdmin):
    def get_readonly_fields(self, _, obj=None):
        if obj:  # editing an existing object
            return self.readonly_fields + ("name",)
        return self.readonly_fields


@admin.action(description="Expire user account")
def expire_user_action(modeladmin, request, queryset):
    for user in queryset.filter(is_active=True):
        AuthToken.objects.filter(user=user).delete()
        user.is_staff = False
        user.is_superuser = False
        user.is_active = False
        for group in user.groups.all():
            group.user_set.remove(user)
        for permission in user.user_permissions.all():
            user.user_permissions.remove(permission)
        user.save()


class CustomGroupAdminForm(forms.ModelForm):
    users = forms.ModelMultipleChoiceField(
        User.objects.all(),
        widget=admin.widgets.FilteredSelectMultiple("Users", False),
        required=False,
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.instance.pk:
            initial_users = self.instance.user_set.values_list("pk", flat=True)
            self.initial["users"] = initial_users

    def save(self, *args, **kwargs):
        kwargs["commit"] = True
        return super().save(*args, **kwargs)

    def save_m2m(self):
        self.instance.user_set.clear()
        self.instance.user_set.add(*self.cleaned_data["users"])


class CustomGroupAdmin(GroupAdmin):
    form = CustomGroupAdminForm


class RemoteArtifactsAuthInline(admin.TabularInline):
    model = RemoteArtifactsAuth
    extra = 0


class CustomUserAdmin(UserAdmin):
    actions = [expire_user_action]

    def has_delete_permission(self, request, obj=None):
        return settings.ALLOW_ADMIN_DELETE

    inlines = [RemoteArtifactsAuthInline]


#  Setup the override in the django admin interface at startup.
admin.site.unregister(User)
admin.site.register(User, CustomUserAdmin)
admin.site.unregister(Group)
admin.site.register(Group, CustomGroupAdmin)


@admin.action(description="cancel selected jobs")
def cancel_action(modeladmin, request, queryset):
    with transaction.atomic():
        for testjob in queryset:
            # TODO: hacky. until django 2.0+ is used and select_for_update gets
            # the 'of' argument.
            warnings.warn(
                "select_for_update should be applied to the whole queryset at once, once we upgrade to django 2+",
                DeprecationWarning,
            )
            testjob = TestJob.objects.select_for_update().get(pk=testjob.pk)
            if testjob.can_cancel(request.user):
                if testjob.is_multinode:
                    for job in testjob.sub_jobs_list:
                        fields = job.go_state_canceling()
                        job.save(update_fields=fields)
                else:
                    fields = testjob.go_state_canceling()
                    testjob.save(update_fields=fields)


@admin.action(description="fail selected jobs")
def fail_action(modeladmin, request, queryset):
    if request.user.is_superuser:
        with transaction.atomic():
            for testjob in queryset.filter(state=TestJob.STATE_CANCELING):
                # TODO: hacky. until django 2.0+ is used and select_for_update
                # gets the 'of' argument.
                warnings.warn(
                    "select_for_update should be applied to the whole queryset at once, once we upgrade to django 2+",
                    DeprecationWarning,
                )
                testjob = TestJob.objects.select_for_update().get(pk=testjob.pk)
                fields = testjob.go_state_finished(TestJob.HEALTH_INCOMPLETE)
                testjob.save(update_fields=fields)


class ActiveDevicesFilter(admin.SimpleListFilter):
    title = "Active devices"
    parameter_name = "state"

    def lookups(self, request, model_admin):
        return (("NoRetired", "Exclude retired"), ("CurrentJob", "With a current Job"))

    def queryset(self, request, queryset):
        if self.value() == "NoRetired":
            return queryset.exclude(health=Device.HEALTH_RETIRED).order_by("hostname")
        if self.value() == "CurrentJob":
            return queryset.filter(
                state__in=[Device.STATE_RESERVED, Device.STATE_RUNNING]
            ).order_by("hostname")


class ActualDeviceFilter(admin.SimpleListFilter):
    title = "Actual Device (except retired)"
    parameter_name = "actual_device"

    def lookups(self, request, model_admin):
        list_of_types = []
        queryset = Device.objects.exclude(health=Device.HEALTH_RETIRED).order_by(
            "hostname"
        )
        for dev_type in queryset:
            list_of_types.append((str(dev_type.hostname), dev_type.hostname))
        return sorted(list_of_types, key=lambda tp: tp[1])

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(actual_device__hostname=self.value())
        return queryset.order_by("actual_device__hostname")


class DeviceTypeFilter(admin.SimpleListFilter):
    title = "Device Type"
    parameter_name = "device_type"

    def lookups(self, request, model_admin):
        list_of_types = []
        queryset = DeviceType.objects.all()
        for dev_type in queryset:
            list_of_types.append((str(dev_type.name), dev_type.name))
        return sorted(list_of_types, key=lambda tp: tp[1])

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(device_type__name=self.value())
        return queryset.order_by("device_type__name")


class RequestedDeviceTypeFilter(admin.SimpleListFilter):
    title = "Requested Device Type"
    parameter_name = "requested_device_type"

    def lookups(self, request, model_admin):
        list_of_types = []
        queryset = DeviceType.objects.order_by("name")
        for dev_type in queryset:
            list_of_types.append((str(dev_type.name), dev_type.name))
        return sorted(list_of_types, key=lambda tp: tp[1])

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(requested_device_type__name=self.value())
        return queryset.order_by("requested_device_type__name")


def _update_devices_health(request, queryset, health):
    with transaction.atomic():
        for device in queryset:
            # TODO: hacky. until django 2.0+ is used and select_for_update gets
            # the 'of' argument.
            warnings.warn(
                "select_for_update should be applied to the whole queryset at once, once we upgrade to django 2+",
                DeprecationWarning,
            )
            device = Device.objects.select_for_update().get(pk=device.pk)

            old_health_display = device.get_health_display()
            device.health = health
            device.save(update_fields=["health"])
            device.log_admin_entry(
                request.user,
                f"{old_health_display} → {device.get_health_display()}",
            )


@admin.action(description="Update health of selected devices to Good")
def device_health_good(modeladmin, request, queryset):
    _update_devices_health(request, queryset, Device.HEALTH_GOOD)


@admin.action(description="Update health of selected devices to Unknown")
def device_health_unknown(modeladmin, request, queryset):
    _update_devices_health(request, queryset, Device.HEALTH_UNKNOWN)


@admin.action(description="Update health of selected devices to Maintenance")
def device_health_maintenance(modeladmin, request, queryset):
    _update_devices_health(request, queryset, Device.HEALTH_MAINTENANCE)


@admin.action(description="Update health of selected devices to Retired")
def device_health_retired(modeladmin, request, queryset):
    _update_devices_health(request, queryset, Device.HEALTH_RETIRED)


@admin.register(Device)
class DeviceAdmin(admin.ModelAdmin):
    list_filter = (
        DeviceTypeFilter,
        "state",
        ActiveDevicesFilter,
        "health",
        "worker_host",
        "is_synced",
    )
    raw_id_fields = ["last_health_report_job"]

    def get_queryset(self, request):
        return (
            super()
            .get_queryset(request)
            .select_related("worker_host", "device_type")
            .prefetch_related(
                Prefetch(
                    "testjobs",
                    queryset=TestJob.objects.filter(~Q(state=TestJob.STATE_FINISHED)),
                    to_attr="running_jobs",
                )
            )
        )

    @admin.display(
        description="HC",
        boolean=True,
    )
    def has_health_check(self, obj):
        return bool(obj.get_health_check())

    @admin.display(
        description="HC enabled",
        boolean=True,
    )
    def health_check_enabled(self, obj):
        return not obj.device_type.disable_health_check

    def get_readonly_fields(self, _, obj=None):
        if obj:  # editing an existing object
            return self.readonly_fields + ("hostname",)
        return self.readonly_fields

    @admin.display(
        description="Config",
        boolean=True,
    )
    def valid_device(self, obj):
        return bool(obj.is_valid())

    def device_dictionary_jinja(self, obj):
        return obj.load_configuration(output_format="raw")

    def has_delete_permission(self, request, obj=None):
        return settings.ALLOW_ADMIN_DELETE

    fieldsets = (
        (
            "Properties",
            {"fields": ("hostname", "device_type", "worker_host", "device_version")},
        ),
        ("Device owner", {"fields": (("physical_owner", "physical_group"),)}),
        (
            "Status",
            {
                "fields": (
                    ("state", "health"),
                    ("last_health_report_job", "current_job"),
                )
            },
        ),
        (
            "Advanced properties",
            {
                "fields": (
                    "description",
                    "is_synced",
                    "tags",
                    ("device_dictionary_jinja"),
                ),
                "classes": ("collapse",),
            },
        ),
    )
    readonly_fields = ("device_dictionary_jinja", "state", "current_job")
    list_display = (
        "hostname",
        "device_type",
        "current_job",
        "worker_host",
        "state",
        "health",
        "has_health_check",
        "health_check_enabled",
        "valid_device",
        "is_synced",
    )
    search_fields = ("hostname", "device_type__name")
    ordering = ["hostname"]
    actions = [
        device_health_good,
        device_health_unknown,
        device_health_maintenance,
        device_health_retired,
    ]
    inlines = [GroupDevicePermissionInline]


@admin.register(TestJob)
class TestJobAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        return (
            super()
            .get_queryset(request)
            .select_related("requested_device_type", "actual_device", "submitter")
        )

    @admin.display(description="Request device type")
    def requested_device_type_name(self, obj):
        return "" if obj.requested_device_type is None else obj.requested_device_type

    def has_add_permission(self, request):
        return False

    def has_delete_permission(self, request, obj=None):
        return settings.ALLOW_ADMIN_DELETE

    actions = [cancel_action, fail_action]
    list_filter = ("state", RequestedDeviceTypeFilter, ActualDeviceFilter, "submitter")
    fieldsets = (
        ("Owner", {"fields": ("submitter", "viewing_groups", "is_public")}),
        ("Request", {"fields": ("requested_device_type", "priority", "health_check")}),
        (
            "Advanced properties",
            {"fields": ("description", "tags", "sub_id", "target_group")},
        ),
        ("Current status", {"fields": ("actual_device", "state", "health")}),
        ("Results & Failures", {"fields": ("failure_tags", "failure_comment")}),
    )
    readonly_fields = ("state",)
    list_display = (
        "id",
        "state",
        "health",
        "submitter",
        "requested_device_type_name",
        "actual_device",
        "health_check",
        "submit_time",
        "start_time",
        "end_time",
    )
    ordering = ["-submit_time"]
    show_full_result_count = False


@admin.action(description="disable health checks")
def disable_health_check_action(modeladmin, request, queryset):
    queryset.update(disable_health_check=False)


@admin.register(DeviceType)
class DeviceTypeAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        return (
            super()
            .get_queryset(request)
            .select_related("architecture", "bits", "processor")
        )

    @admin.display(description="arch")
    def architecture_name(self, obj):
        if obj.architecture:
            return obj.architecture
        return ""

    @admin.display(description="proc")
    def processor_name(self, obj):
        if obj.processor:
            return obj.processor
        return ""

    @admin.display(description="cpu")
    def cpu_model_name(self, obj):
        if obj.cpu_model:
            return obj.cpu_model
        return ""

    @admin.display(description="bits")
    def bit_count(self, obj):
        if obj.bits:
            return obj.bits
        return ""

    @admin.display(description="cores")
    def list_of_cores(self, obj):
        if obj.core_count:
            return "{} x {}".format(
                obj.core_count,
                ",".join([core.name for core in obj.cores.all().order_by("name")]),
            )
        return ""

    def get_readonly_fields(self, _, obj=None):
        if obj:  # editing an existing object
            return self.readonly_fields + ("name",)
        return self.readonly_fields

    @admin.display(
        description="HC enabled",
        boolean=True,
    )
    def health_check_enabled(self, obj):
        return not obj.disable_health_check

    @admin.display(description="HC frequency")
    def health_check_frequency(self, device_type):
        if device_type.health_denominator == DeviceType.HEALTH_PER_JOB:
            return "every %d jobs" % device_type.health_frequency
        return "every %d hours" % device_type.health_frequency

    actions = [disable_health_check_action]
    list_filter = ("name", "display", "cores", "architecture", "processor")
    list_display = (
        "name",
        "display",
        "health_check_enabled",
        "health_check_frequency",
        "architecture_name",
        "processor_name",
        "cpu_model_name",
        "list_of_cores",
        "bit_count",
    )
    fieldsets = (
        ("Properties", {"fields": ("name", "description", "display")}),
        (
            "Health checks",
            {
                "fields": (
                    ("health_frequency", "health_denominator"),
                    "disable_health_check",
                )
            },
        ),
        (
            "Meta data",
            {
                "fields": (
                    "architecture",
                    "processor",
                    ("cores", "core_count"),
                    "bits",
                    "cpu_model",
                )
            },
        ),
    )
    ordering = ["name"]
    inlines = [GroupDeviceTypePermissionInline]


@admin.action(description="Update health of selected workers to Active")
def worker_health_active(ModelAdmin, request, queryset):
    with transaction.atomic():
        for worker in queryset.select_for_update():
            fields = worker.go_health_active(request.user)
            worker.save(update_fields=fields)


@admin.action(description="Update health of selected workers to Maintenance")
def worker_health_maintenance(ModelAdmin, request, queryset):
    with transaction.atomic():
        for worker in queryset.select_for_update():
            fields = worker.go_health_maintenance(request.user)
            worker.save(update_fields=fields)


@admin.action(description="Update health of selected workers to Retired")
def worker_health_retired(ModelAdmin, request, queryset):
    with transaction.atomic():
        for worker in queryset.select_for_update():
            fields = worker.go_health_retired(request.user)
            worker.save(update_fields=fields)


@admin.register(Worker)
class WorkerAdmin(admin.ModelAdmin):
    list_display = ("hostname", "state", "health")
    readonly_fields = ("state",)
    ordering = ["hostname"]
    actions = [worker_health_active, worker_health_maintenance, worker_health_retired]
    inlines = [GroupWorkerPermissionInline]

    def get_readonly_fields(self, _, obj=None):
        if obj:  # editing an existing object
            return self.readonly_fields + ("hostname",)
        return self.readonly_fields

    def has_delete_permission(self, request, obj=None):
        return settings.ALLOW_ADMIN_DELETE


class TagLowerForm(forms.ModelForm):
    def clean_name(self):
        name = self.cleaned_data["name"]
        if name != name.lower():
            raise ValidationError("Tag names are case-insensitive.")
        return name


@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    form = TagLowerForm
    list_display = ("name", "description")
    ordering = ["name"]

    def get_readonly_fields(self, _, obj=None):
        if obj:  # editing an existing object
            return self.readonly_fields + ("name",)
        return self.readonly_fields


@admin.register(NotificationRecipient)
class NotificationRecipientAdmin(admin.ModelAdmin):
    def handle(self, obj):
        if obj.method == NotificationRecipient.EMAIL:
            return obj.email_address
        else:
            return f"{obj.irc_handle}@{obj.irc_server_name}"

    list_display = ("method", "handle", "status")
    list_filter = ("method", "status")

    def has_add_permission(self, request):
        return False

    def has_delete_permission(self, request, obj=None):
        return settings.ALLOW_ADMIN_DELETE


admin.site.register(JobFailureTag)
admin.site.register(ProcessorFamily)
