from django.forms import (
    Form,
    ModelForm,
    ModelMultipleChoiceField,
    MultipleChoiceField,
    ModelChoiceField,
    ChoiceField,
    CharField,
    HiddenInput,
    EmailField,
    IntegerField,
    DateField,
    SplitDateTimeField,
    SplitDateTimeWidget,
    
)
from django.forms import inlineformset_factory
from atlas.models import CaseCollection
from django.contrib.auth.models import User

from generic.models import (
    CidUser,
    CidUserGroup,
    Examination,
    ExamCollection,
    QuestionNote,
    Supervisor,
    UserGrades,
    UserProfile,
    UserUserGroup,
    QuestionReview
)

from django.contrib.admin.widgets import FilteredSelectMultiple
from django.forms.widgets import RadioSelect, Textarea

from rapids.models import Rapid
from anatomy.models import AnatomyQuestion
from longs.models import Long

from django.shortcuts import render, get_object_or_404, redirect

from django.contrib.contenttypes.models import ContentType


from physics.models import UserAnswer as PhysicsUserAnswer, Question
from physics.models import Exam as PhysicsExam
from physics.models import Question as SbasQuestion

from anatomy.models import UserAnswer as AnatomyUserAnswer
from anatomy.models import Exam as AnatomyExam
from anatomy.models import AnatomyQuestion
from anatomy.models import Answer as AnatomyAnswer

from rapids.models import UserAnswer as RapidsUserAnswer
from rapids.models import Exam as RapidsExam
from rapids.models import Rapid
from rapids.models import Answer as RapidAnswer

from shorts.models import UserAnswer as ShortsUserAnswer
from shorts.models import Exam as ShortsExam
from shorts.models import Question as ShortsQuestion

from longs.models import UserAnswer as LongsUserAnswer
from longs.models import Exam as LongsExam
from longs.models import Long

from sbas.models import UserAnswer as SbasUserAnswer
from sbas.models import Exam as SbasExam
from sbas.models import Question as SbasQuestion
from django.core.exceptions import ValidationError
import datetime
from django.forms.utils import from_current_timezone, to_current_timezone

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Layout, Fieldset, Field, Div
from crispy_forms.bootstrap import Accordion, AccordionGroup

from dal import autocomplete
from autocomplete import widgets as htmx_widgets, Autocomplete, AutocompleteWidget, ModelAutocomplete, register as autocomplete_register
from django.utils.safestring import mark_safe
from django.urls import reverse
from django.contrib.auth.models import User


class SplitDateTimeFieldDefaultTime(SplitDateTimeField):
    def compress(self, data_list):
        if data_list:
            # Raise a validation error if time or date is empty
            # (possible if SplitDateTimeField has required=False).
            if data_list[0] in self.empty_values:
                raise ValidationError(
                    self.error_messages["invalid_date"], code="invalid_date"
                )
            if data_list[1] in self.empty_values:
                data_list[1] = datetime.time(0, 1)
                # raise ValidationError(
                #    self.error_messages["invalid_time"], code="invalid_time"
                # )
            result = datetime.datetime.combine(*data_list)
            return from_current_timezone(result)
        return None


class SplitDateTimeFieldDefaultTimeEnd(SplitDateTimeField):
    def compress(self, data_list):
        if data_list:
            # Raise a validation error if time or date is empty
            # (possible if SplitDateTimeField has required=False).
            if data_list[0] in self.empty_values:
                raise ValidationError(
                    self.error_messages["invalid_date"], code="invalid_date"
                )
            if data_list[1] in self.empty_values:
                data_list[1] = datetime.time(23, 59)
                # raise ValidationError(
                #    self.error_messages["invalid_time"], code="invalid_time"
                # )
            result = datetime.datetime.combine(*data_list)
            return from_current_timezone(result)
        return None


class ExamFormMixin:
    def __init__(self, *args, user, **kwargs) -> None:
        super(ModelForm, self).__init__(*args, **kwargs)

        self.helper = FormHelper()
        self.helper.form_id = "id-exam-form"
        self.helper.form_class = "exam-form"
        self.helper.form_method = "post"
        self.helper.form_action = "submit"

        self.helper.layout = Layout(
            Fieldset(
                "Exam Details",
                "name",
                "time_limit",
                "open_access",
                "authors_only",
                "exam_mode",
                "exam_open_access",
                "include_history",
                "active",
                "publish_results",
                "archive",
                Accordion(
                    AccordionGroup("Date restrictions",
                    "restrict_to_dates",
                    "start_date",
                    "end_date",
                    active=False,
                    )
                ),
            ),
            Div(
                Submit("submit", "Submit"),
                css_class="form-group",
            ),
        )

        # self.helper.add_input(Submit("submit", "Submit"))

        # if user.is_superuser or user.groups.filter(name="cid_user_manager").exists():
        #    cid_user_group_queryset = CidUserGroup.objects.filter(archive=False)
        #    user_user_group_queryset = UserUserGroup.objects.filter(archive=False)
        # else:
        #    cid_user_group_queryset = CidUserGroup.objects.none()
        #    user_user_group_queryset = UserUserGroup.objects.none()

        # self.fields["cid_user_groups"] = ModelMultipleChoiceField(
        #    required=False,
        #    queryset=cid_user_group_queryset,
        # )
        # self.fields["user_user_groups"] = ModelMultipleChoiceField(
        #    required=False,
        #    queryset=user_user_group_queryset,
        # )
        self.fields["start_date"] = SplitDateTimeFieldDefaultTime(
            widget=SplitDateTimeWidget(
                date_attrs={"type": "date", "class": "datepicker"},
                time_attrs={"type": "time", "class": "timepicker"},
                date_format="%Y-%m-%d",
                time_format="%H:%M",
            ),
            required=False,
            help_text="The date the exam is due to start (time is optional)",
        )
        self.fields["end_date"] = SplitDateTimeFieldDefaultTimeEnd(
            widget=SplitDateTimeWidget(
                date_attrs={"type": "date", "class": "datepicker"},
                time_attrs={"type": "time", "class": "timepicker"},
                date_format="%Y-%m-%d",
                time_format="%H:%M",
            ),
            required=False,
            help_text="The date the exam is due to start (time is optional)",
        )

    class Meta:
        fields = [
            "name",
            "time_limit",
            "open_access",
            "authors_only",
            "exam_mode",
            "exam_open_access",
            "include_history",
            "restrict_to_dates",
            "start_date",
            "end_date",
            "active",
            "publish_results",
            "archive",
            "results_supervisor_visible"
            # "cid_user_groups",
            # "user_user_groups",
            # "author",
        ]

        # widgets = {
        #    "start_date": TextInput(attrs={"type": "date"}),
        #    "end_date": TextInput(attrs={"type": "date"}),
        # }

    def save(self, commit=True):
        instance = ModelForm.save(self, False)
        instance.save()

        self.save_m2m()

        return instance


class ExamAuthorFormMixin(ModelForm):
    class Meta:
        fields = ["author"]

    def __init__(self, *args, **kwargs):
        ModelForm.__init__(self, *args, **kwargs)
        self.fields["author"] = ModelMultipleChoiceField(
            queryset=User.objects.all(),
            widget=FilteredSelectMultiple(verbose_name="Authors", is_stacked=False),
        )


class ExamMarkerFormMixin(ModelForm):
    class Meta:
        fields = ["markers"]

    def __init__(self, *args, **kwargs):
        ModelForm.__init__(self, *args, **kwargs)
        self.fields["markers"] = ModelMultipleChoiceField(
            queryset=User.objects.all(),
            widget=FilteredSelectMultiple(verbose_name="Markers", is_stacked=False),
        )
        self.fields["markers"].required = False


class ExamGroupsFormMixin(ModelForm):
    class Meta:
        # model = ExamCollection
        fields = ("cid_user_groups", "user_user_groups")

    def __init__(self, *args, user, **kwargs) -> None:
        super(ModelForm, self).__init__(*args, **kwargs)

        # TODO: decide how we should handle managing user groups
        # i.e who should be able to access a list of all users?

        if user.is_superuser or user.groups.filter(name="cid_user_manager").exists():
            cid_user_group_queryset = CidUserGroup.objects.filter(archive=False)
            user_user_group_queryset = UserUserGroup.objects.filter(archive=False)
        else:
            cid_user_group_queryset = CidUserGroup.objects.filter(
                archive=False, open_access=True
            )
            user_user_group_queryset = UserUserGroup.objects.filter(
                archive=False, open_access=True
            )
        # cid_user_group_queryset = CidUserGroup.objects.none()
        # user_user_group_queryset = UserUserGroup.objects.none()

        self.fields["cid_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=cid_user_group_queryset,
            label="CID User Groups",
        )
        self.fields["user_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=user_user_group_queryset,
            label="User Groups",
        )
        self.fields["cid_user_groups"].widget.attrs["size"] = 8
        self.fields["user_user_groups"].widget.attrs["size"] = 8


class ExamCollectionBulkAddGroupsForm(Form):
    """Form used by the HTMX modal to bulk-add CID/User groups to an ExamCollection.

    Accepts `user` kwarg in __init__ to limit available groups based on permissions
    (mirrors behaviour of `ExamGroupsFormMixin`).
    """
    cid_user_groups = ModelMultipleChoiceField(required=False, queryset=CidUserGroup.objects.none(), label="CID User Groups")
    user_user_groups = ModelMultipleChoiceField(required=False, queryset=UserUserGroup.objects.none(), label="User Groups")
    EXAM_TYPE_CHOICES = [
        ("all", "All"),
        ("anatomy", "Anatomy"),
        ("longs", "Longs"),
        ("rapids", "Rapids"),
        ("shorts", "Shorts"),
        ("physics", "Physics"),
        ("sbas", "SBAs"),
    ]
    exam_types = MultipleChoiceField(required=False, choices=EXAM_TYPE_CHOICES, label="Exam types (leave empty for all)")

    def __init__(self, *args, user=None, **kwargs):
        # keep compatibility with other forms that pass user as kwarg
        # This is a plain Form (not a ModelForm) so call Form.__init__
        Form.__init__(self, *args, **kwargs)
        # choose queryset depending on permissions
        if user and (user.is_superuser or user.groups.filter(name="cid_user_manager").exists()):
            cid_qs = CidUserGroup.objects.filter(archive=False)
            user_qs = UserUserGroup.objects.filter(archive=False)
        else:
            cid_qs = CidUserGroup.objects.filter(archive=False, open_access=True)
            user_qs = UserUserGroup.objects.filter(archive=False, open_access=True)

        self.fields["cid_user_groups"].queryset = cid_qs
        self.fields["user_user_groups"].queryset = user_qs
        self.fields["cid_user_groups"].widget.attrs["size"] = 8
        self.fields["user_user_groups"].widget.attrs["size"] = 8

        # Crispy helper: render fields prettily in templates. We set form_tag=False
        # because the HTMX partial includes the <form> tag with hx attributes.
        try:
            self.helper = FormHelper()
            # render only the fields (the template contains the form element)
            self.helper.form_tag = False
            # use bootstrap5 templates if available in the project
            self.helper.template_pack = "bootstrap5"
            self.helper.form_class = "form-vertical"
            self.helper.layout = Layout(
                Div(
                    Field("cid_user_groups", css_class="form-select", template="bootstrap5/layout/multi_select.html"),
                    css_class="mb-3",
                ),
                Div(
                    Field("user_user_groups", css_class="form-select", template="bootstrap5/layout/multi_select.html"),
                    css_class="mb-3",
                ),
                Div(
                    Field("exam_types", css_class="form-select"),
                    css_class="mb-3",
                ),
            )
        except Exception:
            # If crispy isn't available for some reason, degrade gracefully.
            self.helper = None


class ExaminationForm(ModelForm):
    def __init__(self, *args, **kwargs):
        # Where is this coming from?
        if "user" in kwargs:
            user = kwargs.pop("user")

        ModelForm.__init__(self, *args, **kwargs)
        super(ExaminationForm, self).__init__(*args, **kwargs)

    class Meta:
        model = Examination
        fields = ["examination", "modality"]


class ExaminationMergeForm(Form):
    examination_to_merge_into = ModelChoiceField(queryset=Examination.objects.all())


class QuestionNoteForm(ModelForm):
    class Meta:
        model = QuestionNote
        fields = ["content_type", "object_id", "author", "note_type", "note"]
        widgets = {"content_type": HiddenInput(), "object_id": HiddenInput()}

    def __init__(self, *args, **kwargs):
        question_type = kwargs.pop("question_type")
        pk = kwargs.pop("pk")
        user = kwargs.pop("user")

        # if question_type == "rapid":
        #    question = Rapid
        #    content_type = ContentType.objects.get(model="rapid")
        # elif question_type == "anatomy":
        #    question = AnatomyQuestion
        #    content_type = ContentType.objects.get(model="anatomyquestion")
        # elif question_type == "long":
        #    question = Long
        #    content_type = ContentType.objects.get(model="long")
        # else:
        #    raise PermissionError()

        # get_object_or_404(question, pk=pk)
        # We get the 'initial' keyword argument or initialize it
        # as a dict if it didn't exist.
        # if kwargs.get("instance"):
        ModelForm.__init__(self, *args, **kwargs)
        super(QuestionNoteForm, self).__init__(*args, **kwargs)

        # initial = kwargs.setdefault("initial", {})
        ## The widget for a ModelMultipleChoiceField expects
        ## a list of primary key for the selected data.
        ##initial["content_type"] = question
        ##initial["content_id"] = pk
        # self.fields["content_type"].initial = content_type
        # self.fields["object_id"].initial = pk


class CidUserForm(ModelForm):
    class Meta:
        model = CidUser
        fields = [
            "passcode",
            "active",
            "internal_candidate",
            "name",
            "email",
            "supervisor",
            "login_email_sent",
            "results_email_sent",
            "group",
        ]


class CidUserExamForm(ModelForm):
    def __init__(self, *args, **kwargs):
        if kwargs.get("instance"):
            # We get the 'initial' keyword argument or initialize it
            # as a dict if it didn't exist.
            initial = kwargs.setdefault("initial", {})
            # The widget for a ModelMultipleChoiceField expects
            # a list of primary key for the selected data.
            initial["physics_exams"] = [
                t.pk for t in kwargs["instance"].physics_exams.all()
            ]
            initial["anatomy_exams"] = [
                t.pk for t in kwargs["instance"].anatomy_exams.all()
            ]
            initial["sba_exams"] = [t.pk for t in kwargs["instance"].sba_exams.all()]
            initial["rapid_exams"] = [
                t.pk for t in kwargs["instance"].rapid_exams.all()
            ]
            initial["longs_exams"] = [
                t.pk for t in kwargs["instance"].longs_exams.all()
            ]
            initial["casecollection_exams"] = [
                t.pk for t in kwargs["instance"].casecollection_exams.all()
            ]

        ModelForm.__init__(self, *args, **kwargs)

        super(CidUserExamForm, self).__init__(*args, **kwargs)

        self.fields["physics_exams"] = ModelMultipleChoiceField(
            required=False,
            queryset=PhysicsExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(
                verbose_name="Physics Exams", is_stacked=False
            ),
        )
        self.fields["anatomy_exams"] = ModelMultipleChoiceField(
            required=False,
            queryset=AnatomyExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(
                verbose_name="Anatomy Exams", is_stacked=False
            ),
        )
        self.fields["sba_exams"] = ModelMultipleChoiceField(
            required=False,
            queryset=SbasExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(verbose_name="SBA Exams", is_stacked=False),
        )
        self.fields["rapid_exams"] = ModelMultipleChoiceField(
            required=False,
            queryset=RapidsExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(verbose_name="Rapid Exams", is_stacked=False),
        )
        self.fields["longs_exams"] = ModelMultipleChoiceField(
            required=False,
            queryset=LongsExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(verbose_name="Long Exams", is_stacked=False),
        )
        self.fields["casecollection_exams"] = ModelMultipleChoiceField(
            required=False,
            queryset=CaseCollection.objects.filter(archive=False),
            widget=FilteredSelectMultiple(
                verbose_name="Case Collection", is_stacked=False
            ),
        )

    def save(self, commit=True):
        # Get the unsaved Long instance
        instance = ModelForm.save(self, False)

        # Prepare a 'save_m2m' method for the form,
        old_save_m2m = self.save_m2m

        def save_m2m():
            old_save_m2m()
            instance.physics_exams.clear()
            for physics_exams in self.cleaned_data["physics_exams"]:
                instance.physics_exams.add(physics_exams.pk)
            instance.anatomy_exams.clear()
            for anatomy_exams in self.cleaned_data["anatomy_exams"]:
                instance.anatomy_exams.add(anatomy_exams.pk)
            instance.sba_exams.clear()
            for sba_exams in self.cleaned_data["sba_exams"]:
                instance.sba_exams.add(sba_exams.pk)
            instance.rapid_exams.clear()
            for rapid_exams in self.cleaned_data["rapid_exams"]:
                instance.rapid_exams.add(rapid_exams.pk)
            instance.longs_exams.clear()
            for longs_exams in self.cleaned_data["longs_exams"]:
                instance.longs_exams.add(longs_exams.pk)
            instance.casecollection_exams.clear()
            for casecollection_exams in self.cleaned_data["casecollection_exams"]:
                instance.casecollection_exams.add(casecollection_exams.pk)

        self.save_m2m = save_m2m

        # Do we need to save all changes now?
        # if commit:
        instance.save()
        self.save_m2m()

        return instance

    class Meta:
        model = CidUser
        fields = []


class CidUserGroupModelChoiceField(ModelMultipleChoiceField):
    def label_from_instance(self, obj):
        return f"{obj.cid} [{obj.email}]"


class CidUserGroupForm(ModelForm):
    class Meta:
        model = CidUserGroup
        fields = ["name", "archive", "open_access"]


class UserUserGroupModelChoiceField(ModelMultipleChoiceField):
    def label_from_instance(self, obj):
        return f"{obj.username} ({obj.userprofile.grade}) [{obj.email}]"


class UserSearchSelectMultipleWidget:
    """Reusable lightweight widget renderer for a searchable multi-user selector.

    Usage: instantiate with (name, value) where value is an iterable of user ids
    and call .render() to get the HTML fragment. This mirrors the previous
    nested widget but is available module-wide for reuse.
    """

    def __init__(self, name, value):
        self.name = name
        self.value = value or []

    def render(self):
        field_id = f"id_{self.name}"
        search_id = f"user_search_{self.name}"
        results_id = f"user_search_results_{self.name}"
        selected_list_id = f"selected_users_{self.name}"

        users = User.objects.filter(pk__in=self.value) if self.value else []
        options_html = ""
        selected_html = ""
        for u in users:
            options_html += f'<option value="{u.pk}" selected>{u.get_full_name() or u.username} - {u.email}</option>'
            grade_text = ""
            try:
                up = getattr(u, "userprofile", None)
                if up and getattr(up, "grade", None):
                    g = up.grade
                    grade_text = getattr(g, "name", str(g)) if g is not None else ""
            except Exception:
                grade_text = ""

            selected_html += (
                f'<li id="selected_user_{self.name}_{u.pk}" class="list-group-item d-flex justify-content-between align-items-center">'
                f'<span>{u.get_full_name() or u.username} <small class="text-muted">{u.email}</small>'
                + (f' <small class="text-muted ms-2">{grade_text}</small>' if grade_text else '')
                + '</span>'
                f'<button type="button" class="btn btn-sm btn-outline-danger ms-2" onclick="removeUserFromField(\'{self.name}\', {u.pk})">Remove</button>'
                f'</li>'
            )

        url = reverse("generic:user_search_widget")

        grades_options = '<option value="">All grades</option>'
        try:
            from generic.models import UserGrades

            grades_qs = UserGrades.objects.all()
            for g in grades_qs:
                grades_options += f'<option value="{g.pk}">{g.name}</option>'
        except Exception:
            grades_options = '<option value="">All grades</option>'

        html = f"""
        <div class="user-search-widget">
            <select name="{self.name}" id="{field_id}" multiple class="d-none">
                {options_html}
            </select>

            <div class="mb-2">
                <div class="d-flex gap-2 align-items-start">
                    <input type="search" id="{search_id}" class="form-control form-control-sm" placeholder="Search users by name, username or email" />
                    <select id="grade_filter_{self.name}" class="form-select form-select-sm" style="max-width:180px">
                        {grades_options}
                    </select>
                    <!-- Help button for advanced search features -->
                    <button type="button" id="user_search_help_btn_{self.name}" class="btn btn-sm btn-outline-secondary" aria-expanded="false" aria-controls="user_search_help_panel_{self.name}" title="Advanced search help">?</button>
                </div>

                <div id="{results_id}" class="mt-2"></div>

                <!-- Collapsible help panel (hidden by default) -->
                <div id="user_search_help_panel_{self.name}" class="card card-body mt-2 d-none" style="font-size:.9rem;">
                    <strong>Advanced search tips</strong>
                    <ul class="mb-0 mt-1">
                        <li>Basic: type any part of a user's first name, last name, username or email (case-insensitive substring match). Example: <code>smith</code> or <code>joe@example.com</code>.</li>
                        <li>Wildcards: use <code>*</code> or <code>%</code> as the query to return many users (use carefully). Wildcard queries return up to 50 results; normal queries return up to 10.</li>
                        <li>Field-specific searches: prefix with <code>field:term</code> to restrict the search. Supported fields:
                            <ul class="mb-0 mt-1">
                                <li><code>name:</code> or <code>full_name:</code> — matches first OR last name (e.g. <code>name:smith</code>).</li>
                                <li><code>first:</code> or <code>first_name:</code> — matches first name.</li>
                                <li><code>last:</code> or <code>last_name:</code> — matches last name.</li>
                                <li><code>email:</code> — matches email address.</li>
                                <li><code>username:</code> — matches username.</li>
                                <li><code>id:</code> or <code>pk:</code> — match by numeric user id (e.g. <code>id:123</code>).</li>
                                <li><code>grade:</code> — match by grade name or grade id (e.g. <code>grade:ST3</code> or <code>grade:2</code>).</li>
                            </ul>
                        </li>
                        <li>Grade filter: use the grade dropdown next to the search box to narrow results by trainee grade in addition to your query.</li>
                        <li>To add users, click the <em>Add</em> button beside a result. If an <em>Add all results</em> button appears, it will add all currently visible results.</li>
                        <li>If you expect many matches, narrow your query or use a field-specific search to improve relevance and performance.</li>
                    </ul>
                </div>
            </div>

            <div>
                <label class="form-label small">Selected users</label>
                <ul id="{selected_list_id}" class="list-group list-group-flush">
                    {selected_html}
                </ul>
            </div>

            <script>
                (function() {{
                    const search = document.getElementById('{search_id}');
                    const results = document.getElementById('{results_id}');
                    const select = document.getElementById('{field_id}');
                    const selectedList = document.getElementById('{selected_list_id}');
                    const gradeSelect = document.getElementById('grade_filter_{self.name}');
                    const helpBtn = document.getElementById('user_search_help_btn_{self.name}');
                    const helpPanel = document.getElementById('user_search_help_panel_{self.name}');

                    let timeout = null;
                    function fetchResults(q) {{
                        const grade = gradeSelect ? gradeSelect.value : '';
                        if (!q || q.trim().length === 0) {{
                            results.innerHTML = '';
                            return;
                        }}
                        const url = '{url}?field=' + encodeURIComponent('{self.name}') + '&q=' + encodeURIComponent(q) + (grade ? '&grade=' + encodeURIComponent(grade) : '');
                        fetch(url)
                            .then(r => r.text())
                            .then(html => {{ results.innerHTML = html; }});
                    }}

                    search.addEventListener('keyup', function(e) {{
                        clearTimeout(timeout);
                        timeout = setTimeout(() => fetchResults(search.value), 300);
                    }});

                    gradeSelect && gradeSelect.addEventListener('change', function(e) {{
                        // re-run search with same query when grade changes
                        clearTimeout(timeout);
                        timeout = setTimeout(() => fetchResults(search.value), 50);
                    }});

                    // Toggle help panel visibility
                    if (helpBtn && helpPanel) {{
                        helpBtn.addEventListener('click', function(e) {{
                            e.preventDefault();
                            helpPanel.classList.toggle('d-none');
                            const expanded = !helpPanel.classList.contains('d-none');
                            helpBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
                        }});
                    }}

                    // fieldName for this widget instance
                    const widgetFieldName = '{self.name}';

                    // Add button click delegation: results list buttons have data attributes
                    results.addEventListener('click', function(e) {{
                        const btn = e.target.closest('.user-add-btn');
                        if (!btn) return;
                        const id = btn.getAttribute('data-user-id');
                        const text = btn.getAttribute('data-user-text') || btn.textContent.trim();
                        const grade = btn.getAttribute('data-user-grade') || '';
                        addUserToField(widgetFieldName, id, text, grade);
                    }});

                    // Wire up the "Add all results" button if present
                    const addAllBtn = results.parentElement.querySelector('.user-add-all-btn');
                    if (addAllBtn) {{
                        addAllBtn.addEventListener('click', function(e) {{
                            e.preventDefault();
                            addAllFromResults(widgetFieldName);
                        }});
                    }}

                    window.addUserToField = function(fieldName, id, text, grade) {{
                        const sel = document.getElementById('id_' + fieldName);
                        if (!sel) return;
                        // prevent duplicate
                        for (let i=0;i<sel.options.length;i++) {{ if (sel.options[i].value == id) return; }}
                        const opt = document.createElement('option'); opt.value = id; opt.selected = true; opt.text = text;
                        sel.appendChild(opt);

                        // add to visible list (include grade if provided)
                        const li = document.createElement('li');
                        li.className = 'list-group-item d-flex justify-content-between align-items-center';
                        li.id = 'selected_user_' + fieldName + '_' + id;
                        const span = document.createElement('span');
                        span.innerHTML = text + (grade ? ' <small class="text-muted ms-2">' + grade + '</small>' : '');
                        const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'btn btn-sm btn-outline-danger ms-2'; btn.innerText = 'Remove';
                        btn.addEventListener('click', function() {{ removeUserFromField(fieldName, id); }});
                        li.appendChild(span); li.appendChild(btn);
                        selectedList.appendChild(li);
                    }}

                    window.removeUserFromField = function(fieldName, id) {{
                        const sel = document.getElementById('id_' + fieldName);
                        if (!sel) return;
                        for (let i=sel.options.length-1;i>=0;i--) {{ if (sel.options[i].value == id) sel.remove(i); }}
                        const li = document.getElementById('selected_user_' + fieldName + '_' + id);
                        if (li && li.parentNode) li.parentNode.removeChild(li);
                    }}

                    window.addAllFromResults = function(fieldName) {{
                        const list = document.getElementById('user_search_results_list_' + fieldName);
                        if (!list) return;
                        const items = list.querySelectorAll('li[data-user-id]');
                        items.forEach(function(it) {{
                            const id = it.getAttribute('data-user-id');
                            const text = it.getAttribute('data-user-text') || it.textContent.trim();
                            const grade = it.getAttribute('data-user-grade') || '';
                            addUserToField(fieldName, id, text, grade);
                        }});
                    }}
                }})();
            </script>
        </div>
        """

        return mark_safe(html)



class UserUserGroupForm(ModelForm):
    # Use module-level UserSearchSelectMultipleWidget for rendering the users field

    users = UserUserGroupModelChoiceField(
        required=False,
        queryset=User.objects.all(),
        # use our lightweight widget rendering
        widget=None,
    )

    # monkey-patch the field's widget rendering when the form is instantiated
    def __init__(self, *args, **kwargs):
        ModelForm.__init__(self, *args, **kwargs)
        # replace widget with callable that renders our HTML
        if 'users' in self.fields:
            orig_field = self.fields['users']

            class RenderWrapper:
                def __init__(self, field):
                    self.field = field

                # Minimal widget-compatible wrapper used by Django templates and
                # form machinery. We implement the small subset of the Widget
                # API that the templates/checks expect: `is_hidden`,
                # `needs_multipart_form`, `render(...)` and `value_from_datadict(...)`.
                is_hidden = False
                needs_multipart_form = False
                use_fieldset = False
                # attrs is expected by BoundField.id_for_label (widget.attrs.get('id'))
                attrs = {}

                def render(self, name, value, attrs=None, renderer=None):
                    # store attrs so template helpers can inspect them
                    self.attrs = attrs or {}
                    value = value or []
                    widget = UserSearchSelectMultipleWidget(name, value)
                    return widget.render()

                def value_from_datadict(self, data, files, name):
                    # data is usually QueryDict with getlist
                    if hasattr(data, 'getlist'):
                        return data.getlist(name)
                    # fallback
                    val = data.get(name)
                    if val is None:
                        return []
                    return [val]

                def id_for_label(self, id_):
                    # Called by BoundField.id_for_label; prefer explicit id in attrs
                    try:
                        if hasattr(self, 'attrs') and self.attrs and self.attrs.get('id'):
                            return self.attrs.get('id')
                    except Exception:
                        pass
                    return id_

                def use_required_attribute(self, initial):
                    # Called by Django to decide whether to add the required HTML attribute.
                    # We defer to the underlying field/widget if available; otherwise
                    # return False to avoid unexpected 'required' attributes.
                    try:
                        if hasattr(self, 'field') and getattr(self, 'field') is not None:
                            # If original field/widget had such method, prefer it.
                            orig_widget = getattr(self.field, 'widget', None)
                            if orig_widget and hasattr(orig_widget, 'use_required_attribute'):
                                return orig_widget.use_required_attribute(initial)
                    except Exception:
                        pass
                    return False

            self.fields['users'].widget = RenderWrapper(orig_field)

    class Meta:
        model = UserUserGroup
        fields = ["name", "archive", "open_access", "users"]


class UserGroupExamForm(ModelForm):
    GROUP_TYPES = [
        "anatomy_user_user_groups",
        "rapid_user_user_groups",
        "shorts_user_user_groups",
        "longs_user_user_groups",
        "physics_user_user_groups",
        "sba_user_user_groups",
    ]

    # users = UserUserGroupModelChoiceField(
    #    required=False,
    #    queryset=User.objects.all(),
    #    widget=FilteredSelectMultiple(verbose_name="Users", is_stacked=False),
    # )
    def __init__(self, *args, **kwargs):
        if kwargs.get("instance"):
            # We get the 'initial' keyword argument or initialize it
            # as a dict if it didn't exist.
            initial = kwargs.setdefault("initial", {})
            # The widget for a ModelMultipleChoiceField expects
            # a list of primary key for the selected data.
            for groups in self.GROUP_TYPES:
                # instance.anatomy_cid_user_groups.clear()
                # obj = getattr(instance,groups)
                initial[groups] = [
                    t.pk for t in getattr(kwargs["instance"], groups).all()
                ]

        ModelForm.__init__(self, *args, **kwargs)

        super(UserGroupExamForm, self).__init__(*args, **kwargs)

        self.fields["anatomy_user_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=AnatomyExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(
                verbose_name="Anatomy Exams", is_stacked=False
            ),
        )
        self.fields["rapid_user_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=RapidsExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(verbose_name="Rapid Exams", is_stacked=False),
        )
        self.fields["shorts_user_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=ShortsExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(verbose_name="Shorts Exams", is_stacked=False),
        )
        self.fields["longs_user_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=LongsExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(verbose_name="Longs Exams", is_stacked=False),
        )
        self.fields["physics_user_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=PhysicsExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(
                verbose_name="Physics Exams", is_stacked=False
            ),
        )
        self.fields["sba_user_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=SbasExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(verbose_name="Sbas Exams", is_stacked=False),
        )

    class Meta:
        model = UserUserGroup
        fields = []

    def save(self, commit=True):
        # Get the unsaved Long instance
        instance = ModelForm.save(self, False)

        # Prepare a 'save_m2m' method for the form,
        old_save_m2m = self.save_m2m

        def save_m2m():
            old_save_m2m()

            for groups in self.GROUP_TYPES:
                # instance.anatomy_cid_user_groups.clear()
                obj = getattr(instance, groups)
                obj.clear()
                for exam in self.cleaned_data[groups]:
                    obj.add(exam.pk)

        self.save_m2m = save_m2m

        # Do we need to save all changes now?
        # if commit:
        instance.save()
        self.save_m2m()

        return instance


class CidGroupExamForm(ModelForm):
    GROUP_TYPES = [
        "anatomy_cid_user_groups",
        "rapid_cid_user_groups",
        "longs_cid_user_groups",
        "physics_cid_user_groups",
        "sba_cid_user_groups",
    ]

    # users = CidUserGroupModelChoiceField(
    #    required=False,
    #    queryset=User.objects.all(),
    #    widget=FilteredSelectMultiple(verbose_name="Users", is_stacked=False),
    def __init__(self, *args, **kwargs):
        if kwargs.get("instance"):
            # We get the 'initial' keyword argument or initialize it
            # as a dict if it didn't exist.
            initial = kwargs.setdefault("initial", {})
            # The widget for a ModelMultipleChoiceField expects
            # a list of primary key for the selected data.
            for groups in self.GROUP_TYPES:
                # instance.anatomy_cid_user_groups.clear()
                # obj = getattr(instance,groups)
                initial[groups] = [
                    t.pk for t in getattr(kwargs["instance"], groups).all()
                ]

        ModelForm.__init__(self, *args, **kwargs)

        super(CidGroupExamForm, self).__init__(*args, **kwargs)

        self.fields["anatomy_cid_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=AnatomyExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(
                verbose_name="Anatomy Exams", is_stacked=False
            ),
        )
        self.fields["rapid_cid_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=RapidsExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(verbose_name="Rapid Exams", is_stacked=False),
        )
        self.fields["longs_cid_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=LongsExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(verbose_name="Longs Exams", is_stacked=False),
        )
        self.fields["physics_cid_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=PhysicsExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(
                verbose_name="Physics Exams", is_stacked=False
            ),
        )
        self.fields["sba_cid_user_groups"] = ModelMultipleChoiceField(
            required=False,
            queryset=SbasExam.objects.filter(archive=False),
            widget=FilteredSelectMultiple(verbose_name="Sbas Exams", is_stacked=False),
        )

    class Meta:
        model = CidUserGroup
        fields = []

    def save(self, commit=True):
        # Get the unsaved Long instance
        instance = ModelForm.save(self, False)

        # Prepare a 'save_m2m' method for the form,
        old_save_m2m = self.save_m2m

        def save_m2m():
            old_save_m2m()

            for groups in self.GROUP_TYPES:
                # instance.anatomy_cid_user_groups.clear()
                obj = getattr(instance, groups)
                obj.clear()
                for exam in self.cleaned_data[groups]:
                    obj.add(exam.pk)

        self.save_m2m = save_m2m

        # Do we need to save all changes now?
        # if commit:
        instance.save()
        self.save_m2m()

        return instance


# class UserForm(ModelForm):
#    class Meta:
#        model = User
#        fields = ["first_name", "last_name", "email"]
#
# class UserProfileForm(ModelForm):
#    class Meta:
#        model = UserProfile
#        fields = ["supervisor_name", "supervisor_email", "registration_number", "peninsula_trainee"]

GRADE_CHOICES = (
    ("ST1", "ST1"),
    ("ST2", "ST2"),
    ("ST3", "ST3"),
    ("ST4", "ST4"),
    ("ST5", "ST5"),
)


class UserUserForm(Form):
    username = EmailField(
        required=True,
        help_text="Username / email should be the same. Ideally this should be an nhs email.",
    )
    first_name = CharField(max_length=255, required=True)
    last_name = CharField(max_length=255, required=True)
    grade = ModelChoiceField(UserGrades.objects.all(), required=False)


class TraineeForm(Form):
    username = EmailField(
        required=True,
        help_text="Username / email should be the same. Ideally this should be an nhs email.",
    )
    first_name = CharField(max_length=255, required=True)
    last_name = CharField(max_length=255, required=True)
    grade = ModelChoiceField(UserGrades.objects.all(), required=False)
    supervisor = ModelChoiceField(
        Supervisor.objects.all().order_by("name"),
        required=False,
        widget=autocomplete.ModelSelect2(url="generic:supervisor-autocomplete"),
    )  # Needs to be a user/object ref

    # class Meta:
    #    widgets = {
    #        "supervisor" : autocomplete.ModelSelect2(url='generic:supervisor-autocomplete')
    #    }


class SupervisorForm(ModelForm):
    class Meta:
        model = Supervisor
        exclude = ("",)


class ExamCollectionForm(ModelForm):
    GROUP_TYPES = (
        ("Rapid Exams", "rapids_exams", RapidsExam),
        ("Shorts Exams", "shorts_exams", ShortsExam),
        ("Long Exams", "longs_exams", LongsExam),
        ("SBA Exams", "sbas_exams", SbasExam),
        ("Physic Exams", "physics_exams", PhysicsExam),
        ("Anatomy Exams", "anatomy_exams", AnatomyExam),
    )

    class Meta:
        model = ExamCollection
        exclude = ("",)

    def __init__(self, *args, **kwargs):
        if kwargs.get("instance"):
            # We get the 'initial' keyword argument or initialize it
            # as a dict if it didn't exist.
            initial = kwargs.setdefault("initial", {})
            # The widget for a ModelMultipleChoiceField expects
            # a list of primary key for the selected data.
            for name, reverse_prop, exam_obj in self.GROUP_TYPES:
                # instance.anatomy_cid_user_groups.clear()
                # obj = getattr(instance,groups)
                initial[reverse_prop] = [
                    t.pk for t in getattr(kwargs["instance"], reverse_prop).all()
                ]

        ModelForm.__init__(self, *args, **kwargs)

        super(ExamCollectionForm, self).__init__(*args, **kwargs)

        for name, reverse_prop, exam_obj in self.GROUP_TYPES:
            self.fields[reverse_prop] = ModelMultipleChoiceField(
                required=False,
                queryset=exam_obj.objects.filter(archive=False),
                widget=FilteredSelectMultiple(verbose_name=name, is_stacked=False),
            )

        self.fields["author"] = ModelMultipleChoiceField(
            queryset=User.objects.all(),
            widget=FilteredSelectMultiple(verbose_name="Authors", is_stacked=False), required=False,
        )

    def save(self, commit=True):
        # Get the unsaved Long instance
        instance = ModelForm.save(self, False)

        # Prepare a 'save_m2m' method for the form,
        old_save_m2m = self.save_m2m

        def save_m2m():
            old_save_m2m()

            for name, reverse_prop, exam_obj in self.GROUP_TYPES:
                # instance.anatomy_cid_user_groups.clear()
                obj = getattr(instance, reverse_prop)
                obj.clear()
                for exam in self.cleaned_data[reverse_prop]:
                    obj.add(exam)

        self.save_m2m = save_m2m

        # Do we need to save all changes now?
        # if commit:
        instance.save()
        self.save_m2m()

        return instance


class ExamCollectionCloneForm(ModelForm):
    class Meta:
        model = ExamCollection
        fields = ("name", "date")


class UserAutocomplete(ModelAutocomplete):
    model = User
    search_attrs = ["first_name", "last_name", "username"]

class UsersAutocompleteForm(Form):
    # Use a ModelMultipleChoiceField so the widget's multiselect option maps to a multi-value field
    users = ModelMultipleChoiceField(
        queryset=User.objects.all(),
        widget=AutocompleteWidget(ac_class=UserAutocomplete, options={"multiselect": True}),
    )

autocomplete_register(UserAutocomplete)

class QuestionReviewForm(ModelForm):
    class Meta:
        model = QuestionReview
        fields = ["status", "comment"]
        widgets = {
                # Use Bootstrap "btn-check" inputs so we can style labels as button toggles
                "status": RadioSelect(attrs={"class": "btn-check", "autocomplete": "off"}),
                "comment": Textarea(attrs={"class": "form-control form-control-sm", "placeholder": "Comment (optional)", "rows": 2, "autofocus": "autofocus"}),
            }

    def __init__(self, *args, **kwargs):
        # Accept an optional `user` for future validation/use
        self.user = kwargs.pop("user", None)
        super().__init__(*args, **kwargs)