from __future__ import annotations

from django import forms
from django.contrib.auth.models import Group

from apps.core.permissions.roles import UserRole
from apps.repository.documents.models import (
    AccessRequest,
    Author,
    Collection,
    CollectionGroupAccess,
    Document,
    DocumentReviewTask,
    DocumentShare,
    Keyword,
    QARecord,
    SavedSearch,
)


# Languages offered for the document `language` field. The model stores the ISO
# code (CharField, default "en"); these drive the searchable Select so it isn't
# an empty dropdown. Ordered by prevalence across the RUFORUM network.
LANGUAGE_CHOICES = [
    ("en", "English"),
    ("fr", "French"),
    ("pt", "Portuguese"),
    ("es", "Spanish"),
    ("sw", "Swahili"),
    ("ar", "Arabic"),
    ("am", "Amharic"),
    ("other", "Other"),
]


class _MultiFileInput(forms.ClearableFileInput):
    allow_multiple_selected = True


class _MultiFileField(forms.FileField):
    """File field that returns the full list of uploaded files instead of one."""

    widget = _MultiFileInput

    def to_python(self, data):
        if data in self.empty_values:
            return []
        if isinstance(data, (list, tuple)):
            files = [super(_MultiFileField, self).to_python(item) for item in data]
        else:
            files = [super().to_python(data)]
        return [f for f in files if f]


class DocumentSubmitForm(forms.ModelForm):
    """Submission form (Phase 2.1) — drag-drop file plus minimal metadata.

    Supports a primary file plus optional additional files for bulk submission
    of related artefacts (PRD REPO-SP1 FRREP-DCI002).
    """

    # Registry-first author picker (FRREP-MCL001). The wizard writes a JSON
    # array of selected/new authors here; `authors_text` survives as the no-JS
    # fallback and the edit-form format. Neither is required at the field level
    # — the ≥1-author rule lives in clean() and spans both channels.
    authors_json = forms.CharField(required=False, widget=forms.HiddenInput())
    authors_text = forms.CharField(
        label="Authors",
        required=False,
        help_text="One per line: 'Last, First | ORCID (optional) | email (optional)'",
        widget=forms.Textarea(attrs={"rows": 3, "class": "w-full"}),
    )
    keywords_text = forms.CharField(
        label="Tags / keywords",
        required=False,
        help_text=(
            "Optional. New tags enter a moderation queue and appear on the "
            "document only after a curator approves them."
        ),
        # Driven by the controlled-vocabulary combobox (FRREP-MCL007), which
        # writes a comma-separated label list back into this hidden field.
        widget=forms.HiddenInput(),
    )
    # FRREP-MCL006 — which collection the auto-classification rules suggested at
    # submit time; lets the view audit an override when the user keeps another.
    suggested_collection_slug = forms.CharField(
        required=False, widget=forms.HiddenInput()
    )
    additional_files = _MultiFileField(
        label="Additional files (optional)",
        required=False,
        help_text="Hold ⌘/Ctrl to select multiple. Each file becomes its own document with the same metadata.",
    )
    # FRREP-DCI002/003 — bulk files are staged out-of-band (each with its own
    # progress bar, like the primary file) and posted here as a JSON array of
    # ``{token, name, metadata}`` objects. ``metadata`` (optional) overrides the
    # shared template per file (#8); absent/blank keys inherit the shared value.
    # The ``additional_files`` multi-input above is the no-JS fallback.
    bulk_files = forms.CharField(required=False, widget=forms.HiddenInput())
    staged_token = forms.CharField(required=False, widget=forms.HiddenInput())
    # Set to "separate" / "version" / "cancel" by the wizard's duplicate-resolution
    # panel after a hash collision is detected (FRREP-DCI011). Empty on the first
    # submit; the view surfaces the interactive choice, then re-submits with the
    # user's disposition.
    duplicate_decision = forms.CharField(required=False, widget=forms.HiddenInput())

    class Meta:
        model = Document
        fields = (
            "title",
            "abstract",
            "file",
            "document_type",
            "license_type",
            "license_notes",
            "language",
            "year",
            "doi",
            "collection",
            "grant_reference",
        )
        widgets = {
            "abstract": forms.Textarea(attrs={"rows": 5, "class": "w-full"}),
            "file": forms.ClearableFileInput(attrs={"class": "w-full"}),
            "collection": forms.Select(attrs={"data-s2": "1"}),
            "document_type": forms.Select(attrs={"data-s2": "1"}),
            "license_type": forms.Select(attrs={"data-s2": "1"}),
            "language": forms.Select(choices=LANGUAGE_CHOICES, attrs={"data-s2": "1"}),
            # Visibility is the KM officer's call, not the uploader's (#17,
            # FRREP-SP2). New docs inherit their collection's visibility via
            # Visibility.INHERIT (ingest_document default); it stays editable on
            # the edit/QA surfaces.
            # FRREP-MCL009 — the RIMS grant picker (a searchable combobox over
            # active/closed funding records) writes the chosen public_grant_id
            # into this hidden field; clean_grant_reference rejects unknown ids.
            "grant_reference": forms.HiddenInput(),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # The wizard's stage 1 uploads the file out-of-band and posts a token
        # back; the form must accept either path.
        self.fields["file"].required = False
        # Core metadata is mandatory at the form level so a submission can't be
        # created with junk/empty metadata (FRREP-DCI010 / MCL010). title and
        # authors_text are already required; make the rest explicit. abstract is
        # enforced via the schema-driven check in clean() (it is blank=True on
        # the model, so ModelForm leaves it optional).
        for required_field in ("document_type", "collection", "license_type"):
            self.fields[required_field].required = True
        # Only public-facing collections appear in the dropdown for non-staff submitters.
        if not self.initial.get("show_internal"):
            self.fields["collection"].queryset = Collection.objects.exclude(
                visibility=Collection.Visibility.INTERNAL
            )
        self._deprecation_warning: str | None = None

    def clean_authors_text(self):
        # The ≥1-author rule now spans the registry picker (authors_json) and
        # this legacy text fallback, so it lives in clean() — not here. We only
        # normalise whitespace; we never run register_author (validation must
        # not write Author rows).
        return (self.cleaned_data.get("authors_text") or "").strip()

    def clean_grant_reference(self):
        # FRREP-MCL009 — a linked grant must resolve to a real active/closed
        # RIMS funding record. Free-typing an unknown id is rejected; blank is
        # fine (grant linkage is optional).
        ref = (self.cleaned_data.get("grant_reference") or "").strip()
        if not ref:
            return ref
        from apps.repository.integration.grant_link import is_linkable_grant

        if not is_linkable_grant(ref):
            raise forms.ValidationError(
                "No active or completed RUFORUM grant matches that reference — "
                "pick one from the list, or leave it blank."
            )
        return ref

    def clean_collection(self):
        # FRREP-MCL012 / SRS A4 — picking a deprecated collection produces a
        # warning (with the suggested replacement), or an error when there's
        # no replacement to suggest.
        collection = self.cleaned_data.get("collection")
        if collection is not None and getattr(collection, "is_deprecated", False):
            replacement = collection.superseded_by
            if replacement is None:
                raise forms.ValidationError(
                    "This collection is deprecated and has no replacement — "
                    "ask a Repository Administrator which collection to use.",
                )
            note = collection.deprecation_message or "This collection is deprecated."
            self._deprecation_warning = (
                f"{note} Consider picking '{replacement.name}' instead — that "
                "is the active replacement collection."
            )
        return collection

    def clean(self):
        cleaned = super().clean()
        if not cleaned.get("file") and not (cleaned.get("staged_token") or "").strip():
            self.add_error("file", "A source file is required.")

        # ≥1 author, from the registry picker or the legacy text fallback
        # (FRREP-DCI010). Read-only: no Author rows are written during validation.
        from apps.repository.documents import services

        n_picked = services.count_payload_authors(cleaned.get("authors_json") or "")
        text = (cleaned.get("authors_text") or "").strip()
        has_text_author = any(
            line.split("|", 1)[0].strip() for line in text.splitlines() if line.strip()
        )
        if n_picked == 0 and not has_text_author:
            self.add_error(
                "authors_json",
                "Add at least one author — search the registry or add a new one.",
            )

        # Enforce the collection's mandatory metadata set *before* ingest, mirroring
        # the QA pre-check so a submission can't be created with empty mandatory
        # fields and then bounce in review (FRREP-DCI010 / MCL010). The collection
        # is known by now, so its MetadataSchema drives the required set.
        collection = cleaned.get("collection")
        for field_code in services.mandatory_metadata_fields(collection):
            if field_code not in self.fields:
                continue
            # If the field already failed its own validation, don't pile on.
            if field_code in self.errors:
                continue
            value = cleaned.get(field_code)
            is_empty = (
                value.strip() == "" if isinstance(value, str) else value in (None, "")
            )
            if is_empty:
                self.add_error(field_code, "This field is required.")
        return cleaned


class DocumentMetadataEditForm(forms.ModelForm):
    """Edit form with optimistic concurrency token (PRD REPO-SP7 FRREP-COL008).

    The hidden ``edit_token`` is set from the document's `updated_at` when the
    form is rendered. On submit the service compares it against the current
    value and refuses the change if a different user updated the record in the
    meantime.

    Authors are managed via the free-text ``authors_text`` field (same format
    as the submit wizard) — one author per line as
    ``Last, First | ORCID | email`` — round-tripped through
    ``services.parse_authors_text`` / ``render_authors_text``.
    """

    edit_token = forms.CharField(
        widget=forms.HiddenInput(),
        required=False,
    )
    authors_text = forms.CharField(
        label="Authors",
        help_text="One per line: 'Last, First | ORCID (optional) | email (optional)'. "
                  "Removing a line removes that author from this document; matches against "
                  "historical names via the alias registry.",
        widget=forms.Textarea(attrs={"rows": 4, "class": "w-full"}),
    )

    class Meta:
        model = Document
        fields = (
            "title",
            "abstract",
            "document_type",
            "license_type",
            "license_notes",
            "language",
            "year",
            "doi",
            "visibility",
            "grant_reference",
        )
        widgets = {
            "abstract": forms.Textarea(attrs={"rows": 5, "class": "w-full"}),
            "document_type": forms.Select(attrs={"data-s2": "1"}),
            "license_type": forms.Select(attrs={"data-s2": "1"}),
            "language": forms.Select(choices=LANGUAGE_CHOICES, attrs={"data-s2": "1"}),
            # FRREP-MCL009 — same hidden-input + JS search-box pattern as the
            # submit form (a plain `forms.Select` with no `choices` rendered an
            # empty, non-functional dropdown here since `grant_reference` is a
            # bare CharField on the model, not an FK).
            "grant_reference": forms.HiddenInput(),
        }

    def __init__(self, *args, can_set_visibility: bool = True, **kwargs):
        super().__init__(*args, **kwargs)
        # FRREP-AC003 — visibility is a collection-manager/admin call, not the
        # submitter's; hide the field entirely for anyone else instead of
        # trusting the client not to tamper with it.
        if not can_set_visibility:
            self.fields.pop("visibility", None)

    def clean_authors_text(self):
        text = (self.cleaned_data.get("authors_text") or "").strip()
        if not any(line.strip() for line in text.splitlines()):
            raise forms.ValidationError("A document needs at least one author.")
        return text

    def clean_grant_reference(self):
        # Mirrors DocumentSubmitForm.clean_grant_reference — the search box only
        # offers linkable grants, but validate server-side too in case the
        # hidden field is tampered with.
        ref = (self.cleaned_data.get("grant_reference") or "").strip()
        if not ref:
            return ref
        from apps.repository.integration.grant_link import is_linkable_grant

        if not is_linkable_grant(ref):
            raise forms.ValidationError(
                "No active or completed RUFORUM grant matches that reference — "
                "pick one from the list, or leave it blank."
            )
        return ref


class QAReviewForm(forms.Form):
    """Bound to :class:`QAReviewView` (``decide_qa``). Beyond ``decision`` +
    ``comments``, dynamically carries one ``BooleanField`` per FRREP-QA003
    checklist criterion (``checklist_<code>``) — see ``QARecord.CHECKLIST_CRITERIA``.
    """

    decision = forms.ChoiceField(
        choices=[
            (QARecord.Decision.APPROVE, "Approve and publish"),
            (QARecord.Decision.REVISION, "Return for revision"),
            (QARecord.Decision.REJECT, "Reject"),
        ]
    )
    comments = forms.CharField(
        widget=forms.Textarea(attrs={"rows": 4, "class": "w-full"}),
        required=False,
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Checklist items are informational for revision/reject, so they're
        # never required at the field level — the approve-only gate lives in
        # clean(), matching how `comments` is conditionally required below.
        for code, label in QARecord.CHECKLIST_CRITERIA:
            self.fields[f"checklist_{code}"] = forms.BooleanField(label=label, required=False)

    def checklist_dict(self) -> dict:
        """The submitted checklist state as ``{code: bool}`` — pass straight
        through to ``services.decide_qa(..., checklist=...)``."""
        return {
            code: bool(self.cleaned_data.get(f"checklist_{code}"))
            for code, _ in QARecord.CHECKLIST_CRITERIA
        }

    def clean(self):
        data = super().clean()
        if data.get("decision") in {QARecord.Decision.REVISION, QARecord.Decision.REJECT}:
            if not (data.get("comments") or "").strip():
                self.add_error("comments", "Comments are required for this decision.")
        if data.get("decision") == QARecord.Decision.APPROVE:
            unchecked = [
                label
                for code, label in QARecord.CHECKLIST_CRITERIA
                if not data.get(f"checklist_{code}")
            ]
            if unchecked:
                self.add_error(
                    None,
                    "All 6 QA checklist items must be checked before you can approve — "
                    "still unchecked: " + "; ".join(unchecked) + ".",
                )
        return data


class QARecommendForm(QAReviewForm):
    """SP6 two-tier review — same rubric + comment contract as the final
    decision form, but the choice labels make clear this is a recommendation
    for the Editor in Chief, not the decision itself."""

    decision = forms.ChoiceField(
        choices=[
            (QARecord.Decision.APPROVE, "Recommend approval"),
            (QARecord.Decision.REVISION, "Recommend revision"),
            (QARecord.Decision.REJECT, "Recommend rejection"),
        ]
    )


class QAComplianceForm(forms.Form):
    """SP6 — Publication Assistant licensing / open-access compliance check."""

    status = forms.ChoiceField(
        choices=[
            (QARecord.ComplianceStatus.PASSED, "Compliance passed"),
            (QARecord.ComplianceStatus.FLAGGED, "Flag a compliance issue"),
        ]
    )
    notes = forms.CharField(
        widget=forms.Textarea(attrs={"rows": 3, "class": "w-full"}),
        required=False,
    )

    def clean(self):
        data = super().clean()
        if data.get("status") == QARecord.ComplianceStatus.FLAGGED:
            if not (data.get("notes") or "").strip():
                self.add_error("notes", "Notes are required when flagging a compliance issue.")
        return data


class WithdrawForm(forms.Form):
    reason = forms.CharField(
        widget=forms.Textarea(attrs={"rows": 4, "class": "w-full"}),
        help_text="A clear, written reason is required (PRD REPO-SP5).",
    )


class ReinstateForm(forms.Form):
    reason = forms.CharField(
        widget=forms.Textarea(attrs={"rows": 3, "class": "w-full"}),
        required=False,
    )


class ShareForm(forms.ModelForm):
    class Meta:
        model = DocumentShare
        fields = ("collaborator", "permission_level")


class AccessRequestForm(forms.ModelForm):
    class Meta:
        model = AccessRequest
        fields = ("justification",)
        widgets = {
            "justification": forms.Textarea(attrs={"rows": 4, "class": "w-full"}),
        }


class VersionUploadForm(forms.Form):
    # FRREP-VL002 — admin-configurable choice between a major version (new
    # integer, e.g. 2.0) and a minor correction (increments the decimal, e.g.
    # 1.0 -> 1.1), reflecting whether the upload is a substantive revision or
    # a small correction (typo/formatting).
    VERSION_TYPE_CHOICES = [
        ("major", "Major version — a substantive revision"),
        ("minor", "Minor correction — typo/formatting fix"),
    ]

    file = forms.FileField()
    version_type = forms.ChoiceField(
        choices=VERSION_TYPE_CHOICES,
        initial="major",
        widget=forms.RadioSelect(
            attrs={"class": "mt-0.5 h-4 w-4 shrink-0 rounded-full border-ruforum-border-strong text-ruforum-primary focus:ring-ruforum-primary"}
        ),
    )
    changelog = forms.CharField(
        widget=forms.Textarea(attrs={"rows": 3, "class": "w-full"}),
        required=False,
    )


class CollectionForm(forms.ModelForm):
    # FRREP-DCI014 — role-based upload-notification recipients. Declared
    # explicitly so the ArrayField renders as a searchable multi-select instead
    # of the default comma-separated text box.
    notify_roles = forms.MultipleChoiceField(
        choices=UserRole.choices,
        required=False,
        widget=forms.SelectMultiple(attrs={"data-s2": "1"}),
        label="Notify roles on upload",
        help_text="Every active user with a selected role is notified when a "
        "document is ingested, in addition to the named recipients above.",
    )

    class Meta:
        model = Collection
        fields = (
            "name",
            "slug",
            "description",
            "parent",
            "visibility",
            "qa_workflow_enabled",
            "qa_turnaround_days",
            "retention_period_days",
            "retention_action",
            "retention_notice_days",
            "metadata_schema",
            "managed_by",
            "upload_notification_recipients",
            "notify_roles",
            "email_ingest_enabled",
            "email_ingest_address",
            "email_ingest_allowed_senders",
        )
        widgets = {
            "description": forms.Textarea(attrs={"rows": 4}),
            "slug": forms.TextInput(attrs={"placeholder": "e.g. theses"}),
            "qa_turnaround_days": forms.NumberInput(attrs={"min": 1, "max": 90}),
            "retention_period_days": forms.NumberInput(attrs={"min": 0}),
            "retention_notice_days": forms.NumberInput(attrs={"min": 0, "max": 365}),
            "email_ingest_address": forms.EmailInput(
                attrs={
                    "placeholder": "repo+theses@iilmp.org",
                    "autocapitalize": "none",
                    "spellcheck": "false",
                }
            ),
            "email_ingest_allowed_senders": forms.Textarea(
                attrs={
                    "rows": 3,
                    "placeholder": "submitter@ruforum.org\n@partner-institution.org",
                }
            ),
        }
        labels = {
            "email_ingest_enabled": "Allow submissions by email",
            "email_ingest_address": "Inbox address for this collection",
            "email_ingest_allowed_senders": "Allowed senders (one per line)",
        }
        help_texts = {
            "email_ingest_allowed_senders": (
                "Either explicit email addresses or @domains. Required when the "
                "inbox is enabled — anything not matching this list is rejected."
            ),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["parent"].empty_label = "Top-level — no parent collection"
        self.fields["metadata_schema"].empty_label = "Use default schema"
        # Searchable single-selects for the long pickers.
        for name in ("parent", "metadata_schema"):
            self.fields[name].widget.attrs["data-s2"] = "1"
        # Restrict the user pickers to staff-eligible accounts so the dropdown
        # isn't a list of every visitor in the system.
        from django.contrib.auth import get_user_model

        User = get_user_model()
        eligible = User.objects.filter(is_active=True).order_by("first_name", "last_name", "email")
        for name in ("managed_by", "upload_notification_recipients"):
            field = self.fields[name]
            field.queryset = eligible
            field.widget.attrs["data-s2"] = "1"

    def clean(self):
        data = super().clean()
        if data.get("email_ingest_enabled"):
            if not data.get("email_ingest_address"):
                self.add_error(
                    "email_ingest_address",
                    "An inbox address is required when email submissions are enabled.",
                )
            if not (data.get("email_ingest_allowed_senders") or "").strip():
                self.add_error(
                    "email_ingest_allowed_senders",
                    "Provide at least one allowed sender or domain — leaving this blank denies all submissions.",
                )
        return data


class CommentForm(forms.Form):
    text = forms.CharField(widget=forms.Textarea(attrs={"rows": 3, "class": "w-full"}))
    section_anchor = forms.CharField(required=False)
    parent_id = forms.IntegerField(required=False, widget=forms.HiddenInput)


class ReviewTaskForm(forms.ModelForm):
    class Meta:
        model = DocumentReviewTask
        fields = ("assignee", "scope", "instructions", "due_date")
        widgets = {
            "assignee": forms.Select(attrs={"data-s2": "1", "data-s2-placeholder": "Search reviewers…"}),
            "scope": forms.Textarea(attrs={"rows": 2, "class": "w-full"}),
            "instructions": forms.Textarea(attrs={"rows": 3, "class": "w-full"}),
            "due_date": forms.DateInput(attrs={"type": "date"}),
        }


class SavedSearchForm(forms.ModelForm):
    class Meta:
        model = SavedSearch
        fields = ("name", "alert_enabled")


class CollectionGroupAccessForm(forms.ModelForm):
    """Grant / refresh group access to a (typically Restricted) Collection — FRREP-AC005."""

    class Meta:
        model = CollectionGroupAccess
        fields = ("group", "permission_level", "expires_at", "note")
        widgets = {
            "expires_at": forms.DateTimeInput(
                attrs={"type": "datetime-local", "class": "w-full"}
            ),
            "note": forms.TextInput(
                attrs={
                    "class": "w-full",
                    "placeholder": "e.g. Pilot cohort access — review Q3 2026",
                    "maxlength": 255,
                }
            ),
        }
        labels = {
            "group": "User group",
            "permission_level": "Permission level",
            "expires_at": "Expires (optional)",
            "note": "Note for collection managers (optional)",
        }
        help_texts = {
            "expires_at": "Leave blank for an indefinite grant. After this datetime the grant is treated as inactive.",
        }

    def __init__(self, *args, collection: Collection | None = None, **kwargs):
        super().__init__(*args, **kwargs)
        self._collection = collection
        from django.contrib.auth.models import Group

        self.fields["group"].queryset = Group.objects.order_by("name")
        self.fields["group"].empty_label = "Select a group…"

    def clean_group(self):
        group = self.cleaned_data["group"]
        if (
            self._collection is not None
            and self.instance.pk is None
            and CollectionGroupAccess.objects.filter(
                collection=self._collection, group=group
            ).exists()
        ):
            raise forms.ValidationError(
                "This group already holds an access grant for the collection. "
                "Edit the existing grant instead of creating a new one."
            )
        return group


class GroupCreateForm(forms.ModelForm):
    """Create a plain ``auth.Group`` for use in ``CollectionGroupAccessForm``
    (FRREP-AC005) — previously this required the Django admin."""

    class Meta:
        model = Group
        fields = ("name",)
        widgets = {
            "name": forms.TextInput(
                attrs={"class": "w-full", "placeholder": "e.g. Health Researchers"}
            ),
        }

    def clean_name(self):
        name = (self.cleaned_data.get("name") or "").strip()
        if Group.objects.filter(name__iexact=name).exists():
            raise forms.ValidationError(f"A group named “{name}” already exists.")
        return name


class GroupMemberAddForm(forms.Form):
    """Add one user to a group's membership (FRREP-AC005)."""

    user = forms.ModelChoiceField(
        queryset=None,
        label="Add member",
        empty_label="Search for a user…",
        widget=forms.Select(attrs={"class": "w-full", "data-s2": "1"}),
    )

    def __init__(self, *args, group: Group | None = None, **kwargs):
        super().__init__(*args, **kwargs)
        from django.contrib.auth import get_user_model

        User = get_user_model()
        qs = User.objects.filter(is_active=True).order_by(
            "first_name", "last_name", "email"
        )
        if group is not None:
            qs = qs.exclude(pk__in=group.user_set.values_list("pk", flat=True))
        self.fields["user"].queryset = qs


class AuthorMergeForm(forms.Form):
    """Pick a primary Author and one or more duplicates to fold into it (FRREP-MCL004)."""

    primary = forms.ModelChoiceField(
        queryset=Author.objects.all(),
        empty_label="Select the canonical author…",
        label="Primary author (will be kept)",
        widget=forms.Select(attrs={"class": "w-full", "data-s2": "1"}),
    )
    duplicates = forms.ModelMultipleChoiceField(
        queryset=Author.objects.all(),
        label="Duplicate authors (will be merged into the primary and deleted)",
        widget=forms.SelectMultiple(
            attrs={
                "class": "w-full",
                "size": "8",
                "data-s2": "1",
            }
        ),
        help_text="Hold ⌘/Ctrl to select multiple. ORCID and verified status from the primary are preserved.",
    )
    confirm = forms.BooleanField(
        label="I understand this re-points every authored document and deletes the duplicate Author records.",
        required=True,
    )

    def clean(self):
        data = super().clean()
        primary = data.get("primary")
        dupes = list(data.get("duplicates") or [])
        if primary and primary in dupes:
            raise forms.ValidationError(
                "The primary author cannot also appear in the duplicates list."
            )
        if not dupes:
            raise forms.ValidationError("Pick at least one duplicate author to merge.")
        return data


class CollectionMergeForm(forms.Form):
    """Move every document from one or more duplicate Collections into a primary (FRREP-MCL012)."""

    primary = forms.ModelChoiceField(
        queryset=Collection.objects.all(),
        empty_label="Select the canonical collection…",
        label="Primary collection (will keep its name, slug, and settings)",
        widget=forms.Select(attrs={"class": "w-full", "data-s2": "1"}),
    )
    duplicates = forms.ModelMultipleChoiceField(
        queryset=Collection.objects.all(),
        label="Duplicate collections (documents will be moved here, then deleted)",
        widget=forms.SelectMultiple(
            attrs={"class": "w-full", "size": "8", "data-s2": "1"}
        ),
        help_text=(
            "All collections must share the same visibility level — align them first if not. "
            "Old slugs become 301 redirects so existing URLs survive."
        ),
    )
    confirm = forms.BooleanField(
        label="I understand this moves every document and permanently deletes the duplicate Collections.",
        required=True,
    )

    def clean(self):
        data = super().clean()
        primary = data.get("primary")
        dupes = list(data.get("duplicates") or [])
        if primary and primary in dupes:
            raise forms.ValidationError(
                "The primary collection cannot also appear in the duplicates list."
            )
        if not dupes:
            raise forms.ValidationError(
                "Pick at least one duplicate collection to merge."
            )
        if primary and any(d.visibility != primary.visibility for d in dupes):
            raise forms.ValidationError(
                "Visibility mismatch — all collections in a merge must share the same visibility."
            )
        return data


class CollectionRenameForm(forms.Form):
    """Rename a Collection's slug, recording the old slug as a 301-redirect alias (FRREP-MCL012)."""

    new_slug = forms.SlugField(
        label="New slug",
        max_length=120,
        widget=forms.TextInput(
            attrs={
                "class": "w-full",
                "placeholder": "e.g. theses-and-dissertations",
                "autocapitalize": "none",
                "spellcheck": "false",
            }
        ),
        help_text="Lowercase, hyphenated. The old slug will continue to redirect here.",
    )


class CollectionDeprecateForm(forms.Form):
    """Mark a Collection as deprecated with a user-facing message and an
    optional replacement (FRREP-MCL012, SRS use case A4)."""

    _INPUT_CLS = (
        "mt-2 block w-full rounded-md border border-ruforum-border bg-white "
        "px-3 py-2 font-body text-sm text-ruforum-text "
        "focus:border-ruforum-primary focus:outline-none focus:ring-1 focus:ring-ruforum-primary"
    )

    deprecation_message = forms.CharField(
        label="Deprecation message",
        max_length=255,
        widget=forms.Textarea(
            attrs={
                "rows": 3,
                "class": _INPUT_CLS,
                "placeholder": "Why is this collection being retired? Shown to users at submit time.",
            },
        ),
    )
    superseded_by = forms.ModelChoiceField(
        label="Replacement collection (optional)",
        queryset=Collection.objects.none(),
        required=False,
        empty_label="— No replacement —",
        help_text="Active collection users should pick instead. Surfaced in the warning.",
        widget=forms.Select(attrs={"class": _INPUT_CLS, "data-s2": "1"}),
    )

    def __init__(self, *args, collection: Collection, **kwargs):
        super().__init__(*args, **kwargs)
        self._collection = collection
        self.fields["superseded_by"].queryset = (
            Collection.objects.filter(is_deprecated=False)
            .exclude(pk=collection.pk)
            .order_by("name")
        )

    def clean_superseded_by(self):
        replacement = self.cleaned_data.get("superseded_by")
        if replacement and replacement.pk == self._collection.pk:
            raise forms.ValidationError("A collection cannot supersede itself.")
        return replacement


class KeywordReviewForm(forms.Form):
    """Approve / reject a Keyword suggestion (PRD REPO-SP2 FRREP-MCL007)."""

    DECISION_CHOICES = (
        ("approve", "Approve"),
        ("reject", "Reject"),
    )
    decision = forms.ChoiceField(choices=DECISION_CHOICES)
