import uuid
from django import forms
from django.contrib import admin
from django.core import signing
from django.core.exceptions import ValidationError
from apps.access.authorization import authorized_queryset
from apps.organization.models import ServiceCenter
from apps.service.models import ServiceRepairAction
from .admin import InventoryAdmin, _adopt
from .models import PartsIssue, PartsDisposition, DefectiveRecovery, InventoryLocation, SerializedStockUnit
from . import usage_services as services, usage_queries as queries, request_queries
from .queries import authorized_locations, USABLE_TYPES


class IssueForm(forms.ModelForm):
    revisions = forms.CharField(widget=forms.HiddenInput)

    class Meta:
        model = PartsIssue
        fields = ("reservation", "note")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not self.is_bound:
            self.initial["revisions"] = signing.dumps({str(row.pk): row.updated_at.isoformat() for row in self.fields["reservation"].queryset}, salt="inventory-issue")

    def clean(self):
        cleaned = super().clean()
        try:
            revisions = signing.loads(cleaned.get("revisions", ""), salt="inventory-issue")
            row = cleaned.get("reservation")
            self.expected_revision = revisions[str(row.pk)] if row else None
        except (KeyError, TypeError, signing.BadSignature):
            raise ValidationError("Reservation selection changed; reload and review.")
        return cleaned


class DispositionForm(forms.ModelForm):
    destination = forms.ModelChoiceField(queryset=InventoryLocation.objects.none(), required=False)
    selected_units = forms.ModelMultipleChoiceField(queryset=SerializedStockUnit.objects.none(), required=False)
    command_key = forms.UUIDField(initial=uuid.uuid4, widget=forms.HiddenInput)
    revisions = forms.CharField(widget=forms.HiddenInput)

    class Meta:
        model = PartsDisposition
        fields = ("issue", "kind", "quantity", "repair_action", "note")

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not self.is_bound:
            self.initial["revisions"] = signing.dumps({str(row.pk): f"{row.updated_at.isoformat()}:{row.resolved_quantity}" for row in self.fields["issue"].queryset}, salt="inventory-disposition")

    def clean(self):
        cleaned = super().clean()
        try:
            revisions = signing.loads(cleaned.get("revisions", ""), salt="inventory-disposition")
            row = cleaned.get("issue")
            self.expected_revision = revisions[str(row.pk)] if row else None
        except (KeyError, TypeError, signing.BadSignature):
            raise ValidationError("Issued stock changed; reload and review.")
        if cleaned.get("kind") == "RETURNED" and not cleaned.get("destination"):
            raise ValidationError("Choose a destination for unused stock.")
        return cleaned


class RecoveryForm(forms.ModelForm):
    revisions = forms.CharField(widget=forms.HiddenInput)
    class Meta:
        model = DefectiveRecovery
        fields = ("repair_action", "replacement", "location", "component_description", "observed_identifier", "quantity", "command_key", "note")
        widgets = {"command_key": forms.HiddenInput}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not self.is_bound:
            self.initial["command_key"] = uuid.uuid4()
            self.initial["revisions"] = signing.dumps({str(row.pk): row.updated_at.isoformat() for row in self.fields["repair_action"].queryset}, salt="inventory-recovery")

    def clean(self):
        cleaned = super().clean()
        try:
            revisions = signing.loads(cleaned.get("revisions", ""), salt="inventory-recovery")
            row = cleaned.get("repair_action")
            self.expected_revision = revisions[str(row.pk)] if row else None
        except (KeyError, TypeError, signing.BadSignature):
            raise ValidationError("Repair action changed; reload and review.")
        return cleaned


class UsageAdmin(InventoryAdmin):
    actions = None
    editable = ()

    def has_change_permission(self, request, obj=None):
        return False

    def get_readonly_fields(self, request, obj=None):
        return tuple(field.name for field in self.model._meta.concrete_fields if obj or field.name not in self.editable)

    def get_form(self, request, obj=None, **kwargs):
        return forms.modelform_factory(self.model, fields=[]) if obj else super().get_form(request, obj, **kwargs)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "repair_action":
            centers = authorized_queryset(user=request.user, permission="inventory.consume_parts" if self.model is PartsDisposition else "inventory.recover_defective_parts", queryset=ServiceCenter.objects.all())
            kwargs["queryset"] = ServiceRepairAction.objects.filter(is_active=True, performed_at__isnull=False,
                repair_execution__status="OPEN", repair_execution__service_case__service_center_id__in=centers.values("pk")).select_related("repair_execution__service_case", "repair_action")
        return super().formfield_for_foreignkey(db_field, request, **kwargs)


@admin.register(PartsIssue)
class PartsIssueAdmin(UsageAdmin):
    form = IssueForm
    editable = ("reservation", "note")
    list_display = ("service_case", "recipient", "issuer", "issued_at", "custody_location")

    def get_queryset(self, request):
        return queries.parts_issues(actor=request.user)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "reservation":
            kwargs["queryset"] = request_queries.stock_reservations(actor=request.user, active_only=True)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

    def save_model(self, request, obj, form, change):
        _adopt(obj, services.issue_reserved_parts(actor=request.user, reservation=obj.reservation, note=obj.note, expected_revision=form.expected_revision))


@admin.register(PartsDisposition)
class PartsDispositionAdmin(UsageAdmin):
    form = DispositionForm
    editable = ("issue", "kind", "quantity", "repair_action", "note")
    list_display = ("issue", "kind", "quantity", "actor", "recorded_at")

    def get_queryset(self, request):
        return queries.parts_usage(actor=request.user)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "issue":
            kwargs["queryset"] = queries.parts_issues(actor=request.user, unresolved_only=True)
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

    def get_form(self, request, obj=None, **kwargs):
        form = super().get_form(request, obj, **kwargs)
        if "destination" in form.base_fields:
            form.base_fields["destination"].queryset = authorized_locations(actor=request.user, permission="inventory.return_parts").filter(is_active=True, location_type__in=USABLE_TYPES)
            form.base_fields["selected_units"].queryset = SerializedStockUnit.objects.filter(state="IN_CUSTODY",
                current_location_id__in=authorized_locations(actor=request.user).values("pk")).select_related("spare_part", "current_location")
        return form

    def save_model(self, request, obj, form, change):
        options = dict(actor=request.user, issue=obj.issue, quantity=obj.quantity, note=obj.note,
            command_key=form.cleaned_data["command_key"], units=list(form.cleaned_data["selected_units"]), expected_revision=form.expected_revision)
        row = (services.consume_issued_parts(repair_action=obj.repair_action, **options) if obj.kind == "CONSUMED"
            else services.return_unused_parts(destination=form.cleaned_data["destination"], **options))
        _adopt(obj, row)


@admin.register(DefectiveRecovery)
class DefectiveRecoveryAdmin(UsageAdmin):
    form = RecoveryForm
    editable = RecoveryForm.Meta.fields
    list_display = ("service_case", "component_description", "quantity", "location", "actor", "recovered_at")

    def get_queryset(self, request):
        return queries.defective_recoveries(actor=request.user)

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == "location":
            kwargs["queryset"] = authorized_locations(actor=request.user, permission="inventory.recover_defective_parts").filter(is_active=True, location_type__in=["QUARANTINE", "DEFECTIVE"])
        if db_field.name == "replacement":
            kwargs["queryset"] = queries.parts_usage(actor=request.user).filter(kind="CONSUMED")
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

    def save_model(self, request, obj, form, change):
        _adopt(obj, services.recover_defective_component(actor=request.user, expected_action_revision=form.expected_revision, **{key: form.cleaned_data[key] for key in self.editable}))
