from django import forms
from django.contrib import admin, messages
from django.core import signing
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect

from . import services
from .identification import validate_configuration
from .models import Device, DeviceIdentifier


class DeviceForm(forms.ModelForm):
    identifier_revision = forms.CharField(required=False, widget=forms.HiddenInput)
    imei1 = forms.CharField(required=False, max_length=128)
    imei2 = forms.CharField(required=False, max_length=128)
    serial = forms.CharField(required=False, max_length=128)
    correction_type = forms.ChoiceField(required=False, choices=[("", "---------"), *DeviceIdentifier.Type.choices])
    correction_value = forms.CharField(required=False, max_length=128)

    class Meta:
        model = Device
        fields = ("product_model", "product_variant")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.instance._state.adding:
            self.fields.pop("correction_type", None)
            self.fields.pop("correction_value", None)
        else:
            self.identifier_snapshot = {
                entry.identifier_type: services._identifier_revision(entry)
                # Use the same prefetched rows as current_identifiers displays.
                # A fresh query here could sign unseen, concurrently newer state.
                for entry in self.instance.identifiers.all() if entry.is_active
            }
            self.initial["identifier_revision"] = signing.dumps(
                [str(self.instance.pk), self.identifier_snapshot], salt="device-identifier-revision")
            # Existing identifiers are never prefilled into editable inputs.
            for field in ("imei1", "imei2", "serial"):
                self.fields.pop(field, None)

    def clean(self):
        data = super().clean()
        if self.instance._state.adding and data.get("product_model"):
            validate_configuration(data["product_model"], {
                "IMEI1": data.get("imei1", ""), "IMEI2": data.get("imei2", ""), "SERIAL": data.get("serial", ""),
            })
        elif bool(data.get("correction_type")) != bool(data.get("correction_value")):
            raise ValidationError("Choose an identifier type and enter its replacement value together.")
        if not self.instance._state.adding and data.get("correction_type"):
            try:
                supplied = signing.loads(data.get("identifier_revision", ""), salt="device-identifier-revision")
            except signing.BadSignature as error:
                raise ValidationError("Reload this Device before correcting an identifier.") from error
            if supplied != [str(self.instance.pk), self.identifier_snapshot]:
                raise ValidationError("Identifiers changed since this page was opened. Reload and review them.")
        return data


@admin.register(Device)
class DeviceAdmin(admin.ModelAdmin):
    form = DeviceForm
    list_display = ("id", "product_model", "product_variant", "current_identifiers", "is_active", "created_at")
    list_filter = ("is_active", "product_model__brand", "product_model")
    search_fields = ("=identifiers__identity_key", "product_model__code", "product_model__name")
    readonly_fields = ("id", "is_active", "created_at", "updated_at", "current_identifiers")
    actions = ("deactivate_selected", "reactivate_selected")

    def get_queryset(self, request):
        return super().get_queryset(request).select_related("product_model__brand", "product_variant").prefetch_related("identifiers")

    @admin.display(description="Current identifiers")
    def current_identifiers(self, obj):
        if obj is None or obj._state.adding:
            return "—"
        return "; ".join(f"{entry.identifier_type}: {entry.value}" for entry in obj.identifiers.all() if entry.is_active) or "—"

    def get_readonly_fields(self, request, obj=None):
        return self.readonly_fields + (("product_model", "product_variant") if obj else ())

    def get_fields(self, request, obj=None):
        fields = ["id", "product_model", "product_variant", "is_active", "current_identifiers"]
        fields += ["identifier_revision", "correction_type", "correction_value"] if obj else ["imei1", "imei2", "serial"]
        return fields + ["created_at", "updated_at"]

    def has_delete_permission(self, request, obj=None):
        return False

    def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
        try:
            return super().changeform_view(request, object_id, form_url, extra_context)
        except ValidationError:
            self.message_user(request, "Registration/correction rejected. Check policy, catalog state and identifier reservations; reload and retry.", messages.ERROR)
            return HttpResponseRedirect(request.path)

    def save_model(self, request, obj, form, change):
        if not change:
            current = services.register_device(product_model=obj.product_model, product_variant=obj.product_variant,
                imei1=form.cleaned_data.get("imei1", ""), imei2=form.cleaned_data.get("imei2", ""),
                serial=form.cleaned_data.get("serial", ""))
        elif form.cleaned_data.get("correction_type"):
            current = services.replace_device_identifier(device=obj,
                identifier_type=form.cleaned_data["correction_type"], new_value=form.cleaned_data["correction_value"],
                expected_identifier_revision=form.identifier_snapshot.get(form.cleaned_data["correction_type"]))
        else:
            # There are no editable Device fields. Refresh rather than writing
            # the instance loaded before a concurrent lifecycle/correction.
            current = Device.objects.get(pk=obj.pk)
        for field in obj._meta.concrete_fields:
            setattr(obj, field.attname, getattr(current, field.attname))
        obj._state = current._state

    def construct_change_message(self, request, form, formsets, add=False):
        # Do not put supplied physical identifiers into Django's admin log.
        return "Registered device." if add else "Reviewed device or corrected an identifier."

    def _lifecycle(self, request, queryset, operation):
        for device in queryset.order_by("product_model_id", "pk"):
            try:
                operation(device=device)
            except ValidationError:
                self.message_user(request, "Device lifecycle change rejected; review catalog and policy configuration.", messages.ERROR)

    @admin.action(description="Deactivate selected devices", permissions=["change"])
    def deactivate_selected(self, request, queryset):
        self._lifecycle(request, queryset, services.deactivate_device)

    @admin.action(description="Reactivate selected devices", permissions=["change"])
    def reactivate_selected(self, request, queryset):
        self._lifecycle(request, queryset, services.reactivate_device)


@admin.register(DeviceIdentifier)
class DeviceIdentifierAdmin(admin.ModelAdmin):
    list_display = ("id", "device", "identifier_type", "value", "is_active", "created_at", "updated_at")
    list_filter = ("identifier_type", "is_active")
    search_fields = ("=identity_key", "=normalized_value")
    list_select_related = ("device__product_model__brand",)
    readonly_fields = ("id", "device", "identifier_type", "value", "normalized_value", "identity_key", "is_active", "created_at", "updated_at")
    actions = None

    def has_add_permission(self, request):
        return False

    def has_change_permission(self, request, obj=None):
        return False

    def has_delete_permission(self, request, obj=None):
        return False


from . import evidence_admin  # noqa: E402,F401
from . import relationship_admin  # noqa: E402,F401
