import json
import re
from datetime import timedelta

from django.contrib import admin
from django.contrib.admin import SimpleListFilter
from django.core.exceptions import PermissionDenied
from django.db.models import Max, Min, Q
from django.http import Http404, HttpResponse, JsonResponse
from django.shortcuts import redirect
from django.template.response import TemplateResponse
from django.urls import path, reverse
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils import timezone
from django.utils.dateparse import parse_date, parse_time
from openpyxl import Workbook
from openpyxl.styles import Font
from unfold.admin import ModelAdmin
from unfold.contrib.filters.admin import DropdownFilter
from unfold.decorators import action

from .base_admin import DepartmentFilter
from ..models import Area, Event, Person, Record, Semester


class AreaFilter(DropdownFilter, SimpleListFilter):
    title = "area"
    parameter_name = "area"

    def lookups(self, request, model_admin):
        try:
            areas = Area.objects.all().order_by("name")
            return [(area.id, area.name) for area in areas]
        except Exception:
            return []

    def queryset(self, request, queryset):
        if self.value():
            try:
                return queryset.filter(area_id=self.value())
            except Exception:
                pass
        return queryset


class SemesterFilter(DropdownFilter, SimpleListFilter):
    title = "semester"
    parameter_name = "semester"

    def lookups(self, request, model_admin):
        try:
            semesters = Semester.objects.all().order_by("-start_date")
            return [(sem.id, sem.title) for sem in semesters]
        except Exception:
            return []

    def queryset(self, request, queryset):
        if self.value():
            try:
                return queryset.filter(semester_id=self.value())
            except Exception:
                pass
        return queryset


class FacilitatorFilter(DropdownFilter, SimpleListFilter):
    title = "facilitator"
    parameter_name = "facilitator"

    def lookups(self, request, model_admin):
        try:
            from django.contrib.auth import get_user_model

            user_model = get_user_model()
            facilitators = user_model.objects.filter(is_staff=True).order_by("username")
            return [(user.id, user.username) for user in facilitators]
        except Exception:
            return []

    def queryset(self, request, queryset):
        if self.value():
            try:
                return queryset.filter(facilitators__id=self.value()).distinct()
            except Exception:
                pass
        return queryset


@admin.register(Event)
class EventAdmin(ModelAdmin):
    list_display = [
        "title",
        "get_facilitators",
        "get_participant_count",
        "area",
        "semester",
        "department",
        "start_datetime",
        "end_datetime",
    ]
    list_filter = [AreaFilter, SemesterFilter, DepartmentFilter, FacilitatorFilter]
    search_fields = ["title", "facilitators__username", "facilitators__first_name", "facilitators__last_name"]
    autocomplete_fields = ["participants", "facilitators"]

    change_list_template = "admin/event_changelist.html"
    change_form_template = "admin/base_change_form.html"
    change_form_show_cancel_button = True

    list_filter_submit = True
    list_fullwidth = False
    list_filter_sheet = False
    list_horizontal_scrollbar_top = False
    list_disable_select_all = False

    superadmin_only_edit_fields = []

    fieldsets = [
        ("Details", {"fields": ["title", "department", "area", "semester"]}),
        ("Schedule", {"fields": ["start_datetime", "end_datetime"]}),
        ("People", {"fields": ["facilitators", "participants"]}),
    ]

    actions_list = []
    actions_row = ["start_scanning_custom_action", "view_attendances_custom_action"]
    actions_detail = ["start_scanning_custom_action", "view_attendances_custom_action"]
    actions_submit_line = []

    def get_queryset(self, request):
        qs = super().get_queryset(request).prefetch_related("facilitators")
        can_view_all_events = request.user.has_perm("core.view_all_events")
        if request.user.is_superuser or can_view_all_events:
            return qs
        return qs.filter(facilitators=request.user).distinct()

    def get_readonly_fields(self, request, obj=None):
        readonly_fields = list(super().get_readonly_fields(request, obj))
        if request.user.is_superuser:
            return readonly_fields
        return list(dict.fromkeys(readonly_fields + self.superadmin_only_edit_fields))

    def save_model(self, request, obj, form, change):
        return super().save_model(request, obj, form, change)

    def save_related(self, request, form, formsets, change):
        super().save_related(request, form, formsets, change)
        if not request.user.is_superuser and not form.instance.facilitators.filter(pk=request.user.pk).exists():
            form.instance.facilitators.add(request.user)

    def changelist_view(self, request, extra_context=None):
        extra_context = extra_context or {}
        active_filters = []

        area_id = request.GET.get("area")
        if area_id:
            from ..models import Area
            area = Area.objects.filter(id=area_id).first()
            active_filters.append({"label": "Area", "value": area.name if area else area_id})

        semester_id = request.GET.get("semester")
        if semester_id:
            from ..models import Semester
            semester = Semester.objects.filter(id=semester_id).first()
            active_filters.append({"label": "Semester", "value": semester.title if semester else semester_id})

        department_id = request.GET.get("department")
        if department_id:
            from ..models import Department
            department = Department.objects.filter(id=department_id).first()
            active_filters.append({"label": "Department", "value": department.name if department else department_id})

        facilitator_id = request.GET.get("facilitator")
        if facilitator_id:
            from django.contrib.auth import get_user_model
            User = get_user_model()
            facilitator = User.objects.filter(id=facilitator_id).first()
            active_filters.append({"label": "Facilitator", "value": facilitator.username if facilitator else facilitator_id})

        search_query = request.GET.get("q")
        if search_query:
            active_filters.append({"label": "Search", "value": search_query})

        extra_context["active_filter_summary"] = active_filters
        return super().changelist_view(request, extra_context=extra_context)

    def get_urls(self):
        urls = super().get_urls()
        custom_urls = [
            path(
                "<path:object_id>/start-scanning/",
                self.admin_site.admin_view(self.start_scanning_view),
                name="core_event_start_scanning",
            ),
            path(
                "<path:object_id>/start-scanning/scan/",
                self.admin_site.admin_view(self.start_scanning_scan_view),
                name="core_event_start_scanning_scan",
            ),
            path(
                "<path:object_id>/view-attendance/",
                self.admin_site.admin_view(self.view_attendance_view),
                name="core_event_view_attendance",
            ),
            path(
                "<path:object_id>/view-attendance/export-excel/",
                self.admin_site.admin_view(self.view_attendance_export_excel_view),
                name="core_event_view_attendance_export_excel",
            ),
            path(
                "<path:object_id>/view-attendance/add-participant/",
                self.admin_site.admin_view(self.view_attendance_add_participant_view),
                name="core_event_view_attendance_add_participant",
            ),
            path(
                "<path:object_id>/view-attendance/add-all-participants/",
                self.admin_site.admin_view(self.view_attendance_add_all_participants_view),
                name="core_event_view_attendance_add_all_participants",
            ),
            path(
                "<path:object_id>/view-attendance/remove-participant/",
                self.admin_site.admin_view(self.view_attendance_remove_participant_view),
                name="core_event_view_attendance_remove_participant",
            ),
        ]
        return custom_urls + urls

    def get_participant_count(self, obj):
        return obj.participants.count()

    get_participant_count.short_description = "Participants"

    def get_facilitators(self, obj):
        facilitators = list(obj.facilitators.all())
        if not facilitators:
            return "N/A"
        return ", ".join(str(user) for user in facilitators)

    get_facilitators.short_description = "Facilitators"

    @action(description="View Attendances", url_path="actions-view-attendances")
    def view_attendances_custom_action(self, request, object_id):
        return redirect(reverse("admin:core_event_view_attendance", args=[object_id]))

    @action(description="Start Scanning", url_path="actions-start-scanning")
    def start_scanning_custom_action(self, request, object_id):
        return redirect(reverse("admin:core_event_start_scanning", args=[object_id]))

    def start_scanning_view(self, request, object_id):
        event_obj = self.get_object(request, object_id)
        if event_obj is None:
            raise Http404("Event not found.")

        if not self.has_view_permission(request, event_obj):
            raise PermissionDenied

        now = timezone.localtime(timezone.now())
        schedule_warnings = []
        is_on_schedule = True
        schedule_summary = "No schedule set"

        if event_obj.start_datetime and event_obj.end_datetime:
            start_local = timezone.localtime(event_obj.start_datetime)
            end_local = timezone.localtime(event_obj.end_datetime)
            schedule_summary = f"{start_local.strftime('%b %d, %Y %I:%M %p')} - {end_local.strftime('%I:%M %p') if start_local.date() == end_local.date() else end_local.strftime('%b %d, %Y %I:%M %p')}"

            # Grace period: 60 minutes before start, 30 minutes after end
            window_start = start_local - timedelta(minutes=60)
            window_end = end_local + timedelta(minutes=30)

            if now < window_start:
                schedule_warnings.append(
                    f"This event is scheduled for {schedule_summary} (Starts in {int((start_local - now).total_seconds() // 3600)}h {int(((start_local - now).total_seconds() % 3600) // 60)}m)."
                )
                is_on_schedule = False
            elif now > window_end:
                schedule_warnings.append(
                    f"This event ended on {end_local.strftime('%b %d, %Y at %I:%M %p')}."
                )
                is_on_schedule = False

        context = {
            **self.admin_site.each_context(request),
            "opts": self.model._meta,
            "title": f"Start Scanning: {event_obj.title}",
            "event_obj": event_obj,
            "scan_url": reverse("admin:core_event_start_scanning_scan", args=[event_obj.pk]),
            "view_attendance_url": reverse("admin:core_event_view_attendance", args=[event_obj.pk]),
            "back_url": reverse("admin:core_event_change", args=[event_obj.pk]),
            "is_on_schedule": is_on_schedule,
            "schedule_warnings": schedule_warnings,
            "schedule_summary": schedule_summary,
            "total_participants": event_obj.participants.count(),
            "current_local_time": now.strftime("%A, %b %d, %Y %I:%M %p"),
        }
        return TemplateResponse(request, "admin/core/event/start_scanning.html", context)

    def start_scanning_scan_view(self, request, object_id):
        if request.method != "POST":
            return JsonResponse({"status": "error", "message": "Method not allowed."}, status=405)

        event_obj = self.get_object(request, object_id)
        if event_obj is None:
            raise Http404("Event not found.")

        if not self.has_view_permission(request, event_obj):
            raise PermissionDenied

        try:
            payload = json.loads(request.body.decode("utf-8"))
        except (json.JSONDecodeError, UnicodeDecodeError):
            return JsonResponse({"status": "error", "message": "Invalid JSON payload."}, status=400)

        scanned_card_number = (payload.get("card_number") or "").strip()
        if not scanned_card_number:
            return JsonResponse({"status": "error", "message": "Card number is required."}, status=400)

        sanitized_hex = re.sub(r"[^0-9A-Fa-f]", "", scanned_card_number)
        colon_lower = ":".join(sanitized_hex[i:i + 2] for i in range(0, len(sanitized_hex), 2)) if sanitized_hex else ""
        colon_upper = colon_lower.upper() if colon_lower else ""

        lookup_candidates = [
            scanned_card_number,
            scanned_card_number.lower(),
            scanned_card_number.upper(),
            sanitized_hex,
            sanitized_hex.lower(),
            sanitized_hex.upper(),
            colon_lower,
            colon_upper,
        ]
        lookup_candidates = [value for value in dict.fromkeys(lookup_candidates) if value]

        person = Person.objects.select_related("department").filter(card_number__in=lookup_candidates).first()

        saved_card_number = person.card_number if person else scanned_card_number
        record = Record.objects.create(card_number=saved_card_number, event=event_obj, user=request.user)

        if person is None:
            return JsonResponse(
                {
                    "status": "success",
                    "known": False,
                    "enrolled": False,
                    "message": "Card saved to event attendance, but person is not registered.",
                    "timestamp": timezone.localtime(record.timestamp).strftime("%Y-%m-%d %H:%M:%S"),
                }
            )

        is_participant = event_obj.participants.filter(pk=person.pk).exists()

        return JsonResponse(
            {
                "status": "success",
                "known": True,
                "enrolled": is_participant,
                "message": "Attendance saved." if is_participant else "Attendance saved (Not in participant roster).",
                "timestamp": timezone.localtime(record.timestamp).strftime("%Y-%m-%d %H:%M:%S"),
                "person": {
                    "id": person.id,
                    "school_id": person.school_id,
                    "name": person.display_name,
                    "department": person.department.name if person.department else None,
                    "image": person.image.url if person.image else None,
                    "card_number": person.card_number,
                },
            }
        )

    def _build_attendance_snapshot(self, event_obj, query_params):
        raw_filter_date_from = (query_params.get("date_from") or "").strip()
        raw_filter_date_to = (query_params.get("date_to") or "").strip()
        event_start_local = timezone.localtime(event_obj.start_datetime) if event_obj.start_datetime else None
        event_end_local = timezone.localtime(event_obj.end_datetime) if event_obj.end_datetime else None

        default_date_from = event_start_local.date() if event_start_local else timezone.localdate()
        default_date_to = event_end_local.date() if event_end_local else default_date_from

        filter_date_from = parse_date(raw_filter_date_from) if raw_filter_date_from else default_date_from
        filter_date_to = parse_date(raw_filter_date_to) if raw_filter_date_to else default_date_to
        raw_filter_time_from = (query_params.get("time_from") or "").strip()
        raw_filter_time_to = (query_params.get("time_to") or "").strip()
        default_time_from = event_start_local.strftime("%H:%M") if event_start_local else ""
        default_time_to = event_end_local.strftime("%H:%M") if event_end_local else ""

        filter_time_from = parse_time(raw_filter_time_from) if raw_filter_time_from else parse_time(default_time_from)
        filter_time_to = parse_time(raw_filter_time_to) if raw_filter_time_to else parse_time(default_time_to)
        filter_device = (query_params.get("device") or "").strip()
        filter_search = (query_params.get("search") or "").strip()
        show_non_participants = (query_params.get("show_non_participants") or "").strip().lower() in {
            "1",
            "true",
            "yes",
            "on",
        }

        participants = list(
            event_obj.participants.select_related("department")
            .order_by("lastname", "firstname", "school_id")
        )
        participant_ids_set = {participant.id for participant in participants}
        participant_cards = [participant.card_number for participant in participants if participant.card_number]
        participant_cards_set = set(participant_cards)

        event_scope_query = Q(event=event_obj)
        # Include scans from the same event area so non-participant attendees
        # can still appear in With Records when they logged within the event window.
        if event_obj.area_id:
            event_scope_query |= Q(device__area_id=event_obj.area_id)

        participant_scope_query = Q(card_number__in=participant_cards) if participant_cards else Q(pk__in=[])

        baseline_records = Record.objects.filter(
            event_scope_query | participant_scope_query
        ).select_related("device", "device__area", "user")

        if event_obj.area_id:
            baseline_records = baseline_records.filter(
                Q(device__area_id=event_obj.area_id) | Q(event=event_obj)
            )

        if event_obj.semester_id and event_obj.semester.start_date and event_obj.semester.end_date:
            baseline_records = baseline_records.filter(
                timestamp__date__gte=event_obj.semester.start_date,
                timestamp__date__lte=event_obj.semester.end_date,
            )

        if event_obj.start_datetime:
            baseline_records = baseline_records.filter(timestamp__gte=event_obj.start_datetime)
        if event_obj.end_datetime:
            baseline_records = baseline_records.filter(timestamp__lte=event_obj.end_datetime)

        available_devices = list(
            baseline_records.exclude(device__isnull=True)
            .values_list("device__device_id", flat=True)
            .distinct()
            .order_by("device__device_id")
        )

        filtered_records = baseline_records
        if filter_device:
            filtered_records = filtered_records.filter(device__device_id=filter_device)
        if filter_date_from:
            filtered_records = filtered_records.filter(timestamp__date__gte=filter_date_from)
        if filter_date_to:
            filtered_records = filtered_records.filter(timestamp__date__lte=filter_date_to)
        if filter_time_from:
            filtered_records = filtered_records.filter(timestamp__time__gte=filter_time_from)
        if filter_time_to:
            filtered_records = filtered_records.filter(timestamp__time__lte=filter_time_to)

        stats_by_card = {
            row["card_number"]: row
            for row in filtered_records.values("card_number").annotate(
                first_seen=Min("timestamp"),
                last_seen=Max("timestamp"),
            )
        }

        source_by_card = {}
        for row in filtered_records.values("card_number", "device__device_id", "device__area__name", "user__last_name"):
            card = row["card_number"]
            device_id = row["device__device_id"]
            area_name = row["device__area__name"]
            user_last_name = row["user__last_name"]

            if user_last_name:
                source_label = f"{user_last_name.upper()}'s DEVICE"
            elif device_id:
                source_label = f"{device_id} / {area_name or 'Unknown Area'}"
            else:
                source_label = "Phone Scan / Unknown Area"

            source_by_card.setdefault(card, set()).add(source_label)

        with_records_rows = []
        with_records_view_rows = []
        participant_no_records_rows = []
        rendered_cards = set()

        # Process event participants: separate those with attendance records from those without
        # With Records: participants who have a matching scan in the filtered records
        # No Records: participants who either have no card or no scan matching the filters
        for participant in participants:
            if filter_search:
                search_haystack = " ".join(
                    [
                        (participant.display_name or "").lower(),
                        (participant.school_id or "").lower(),
                        (participant.card_number or "").lower(),
                    ]
                )
                if filter_search.lower() not in search_haystack:
                    continue

            card_number = participant.card_number or ""
            stat = stats_by_card.get(card_number)

            if stat and card_number:
                # Participant has a valid card number and matching scan record
                first_seen = timezone.localtime(stat["first_seen"]).strftime("%Y-%m-%d %H:%M:%S") if stat["first_seen"] else "-"
                last_seen = timezone.localtime(stat["last_seen"]).strftime("%Y-%m-%d %H:%M:%S") if stat["last_seen"] else "-"
                sources = sorted(source_by_card.get(card_number, []))
                source_text = ", ".join(sources) if sources else "Unknown Source / Unknown Area"
                with_records_rows.append(
                    [
                        participant.school_id or "N/A",
                        participant.lastname or "N/A",
                        participant.firstname or "N/A",
                        first_seen,
                        last_seen,
                        source_text,
                    ]
                )
                with_records_view_rows.append(
                    {
                        "school_id": participant.school_id or "N/A",
                        "name": participant.display_name or "N/A",
                        "first_log": first_seen,
                        "last_log": last_seen,
                        "device_area": source_text,
                        "can_add": False,
                        "can_remove": True,
                        "person_id": participant.id,
                    }
                )
                rendered_cards.add(card_number)
            else:
                # Participant either has no card number or no matching scan in selected filters
                reason = "No card number" if not card_number else "No matching scan in selected filters"
                participant_no_records_rows.append(
                    [
                        participant.school_id or "N/A",
                        participant.lastname or "N/A",
                        participant.firstname or "N/A",
                        reason,
                    ]
                )

        # Process non-participant scans: show attendees who scanned but were not in participant list
        # This allows capturing people who attended even if they weren't formally registered as participants
        extra_cards = [card for card in stats_by_card.keys() if card and card not in rendered_cards]
        if show_non_participants and extra_cards:
            people_by_card = {
                person.card_number: person
                for person in Person.objects.select_related("department").filter(card_number__in=extra_cards)
            }

            for card_number in sorted(extra_cards):
                # Skip if this card belongs to a participant (already processed above)
                if card_number in participant_cards_set:
                    continue

                person = people_by_card.get(card_number)
                if filter_search:
                    search_haystack = " ".join(
                        [
                            (person.display_name if person else "").lower(),
                            (person.school_id if person else "").lower(),
                            (card_number or "").lower(),
                        ]
                    )
                    if filter_search.lower() not in search_haystack:
                        continue

                stat = stats_by_card.get(card_number)
                first_seen = timezone.localtime(stat["first_seen"]).strftime("%Y-%m-%d %H:%M:%S") if stat and stat["first_seen"] else "-"
                last_seen = timezone.localtime(stat["last_seen"]).strftime("%Y-%m-%d %H:%M:%S") if stat and stat["last_seen"] else "-"
                sources = sorted(source_by_card.get(card_number, []))
                source_text = ", ".join(sources) if sources else "Unknown Source / Unknown Area"

                # Add non-participant attendance record to with_records_rows
                with_records_rows.append(
                    [
                        person.school_id if person and person.school_id else "N/A",
                        person.lastname if person else "N/A",
                        person.firstname if person else "N/A",
                        first_seen,
                        last_seen,
                        source_text,
                    ]
                )
                with_records_view_rows.append(
                    {
                        "school_id": person.school_id if person and person.school_id else "N/A",
                        "name": person.display_name if person else "Unknown Person",
                        "first_log": first_seen,
                        "last_log": last_seen,
                        "device_area": source_text,
                        "can_add": bool(person and person.id not in participant_ids_set),
                        "can_remove": False,
                        "person_id": person.id if person else None,
                    }
                )

        active_filter_summary = []
        if filter_date_from:
            active_filter_summary.append(f"From: {filter_date_from.isoformat()}")
        if filter_date_to:
            active_filter_summary.append(f"To: {filter_date_to.isoformat()}")
        if filter_time_from:
            active_filter_summary.append(f"From Time: {filter_time_from.strftime('%H:%M')}")
        if filter_time_to:
            active_filter_summary.append(f"To Time: {filter_time_to.strftime('%H:%M')}")
        if filter_device:
            active_filter_summary.append(f"Device: {filter_device}")
        if filter_search:
            active_filter_summary.append(f"Search: {filter_search}")
        if show_non_participants:
            active_filter_summary.append("Show Non-Participants: Enabled")

        return {
            "with_records_rows": with_records_rows,
            "with_records_view_rows": with_records_view_rows,
            # No Records is intentionally participant-only.
            "no_records_rows": participant_no_records_rows,
            "active_filter_summary": active_filter_summary,
            "filter_device": filter_device,
            "filter_date_from": filter_date_from.isoformat() if filter_date_from else "",
            "filter_date_to": filter_date_to.isoformat() if filter_date_to else "",
            "filter_time_from": raw_filter_time_from or default_time_from,
            "filter_time_to": raw_filter_time_to or default_time_to,
            "filter_search": filter_search,
            "include_non_participants": show_non_participants,
            "available_devices": available_devices,
        }

    def view_attendance_add_participant_view(self, request, object_id):
        if request.method != "POST":
            return JsonResponse({"status": "error", "message": "Method not allowed."}, status=405)

        event_obj = self.get_object(request, object_id)
        if event_obj is None:
            raise Http404("Event not found.")

        if not self.has_change_permission(request, event_obj):
            raise PermissionDenied

        raw_person_id = (request.POST.get("person_id") or "").strip()
        next_url = (request.POST.get("next") or "").strip()
        fallback_url = reverse("admin:core_event_view_attendance", args=[event_obj.pk])

        if not next_url or not url_has_allowed_host_and_scheme(
            url=next_url,
            allowed_hosts={request.get_host()},
            require_https=request.is_secure(),
        ):
            next_url = fallback_url

        try:
            person_id = int(raw_person_id)
        except (TypeError, ValueError):
            return redirect(next_url)

        person = Person.objects.filter(pk=person_id).first()
        if person is None:
            return redirect(next_url)

        if person.is_archived:
            return redirect(next_url)

        if not event_obj.participants.filter(pk=person.pk).exists():
            event_obj.participants.add(person)

        return redirect(next_url)

    def view_attendance_add_all_participants_view(self, request, object_id):
        if request.method != "POST":
            return JsonResponse({"status": "error", "message": "Method not allowed."}, status=405)

        event_obj = self.get_object(request, object_id)
        if event_obj is None:
            raise Http404("Event not found.")

        if not self.has_change_permission(request, event_obj):
            raise PermissionDenied

        next_url = (request.POST.get("next") or "").strip()
        fallback_url = reverse("admin:core_event_view_attendance", args=[event_obj.pk])

        if not next_url or not url_has_allowed_host_and_scheme(
            url=next_url,
            allowed_hosts={request.get_host()},
            require_https=request.is_secure(),
        ):
            next_url = fallback_url

        raw_person_ids = request.POST.getlist("person_ids")
        valid_person_ids = []
        for raw_person_id in raw_person_ids:
            try:
                person_id = int((raw_person_id or "").strip())
            except (TypeError, ValueError):
                continue
            if person_id not in valid_person_ids:
                valid_person_ids.append(person_id)

        if not valid_person_ids:
            return redirect(next_url)

        existing_ids = set(event_obj.participants.filter(pk__in=valid_person_ids).values_list("pk", flat=True))
        ids_to_add = [person_id for person_id in valid_person_ids if person_id not in existing_ids]
        if ids_to_add:
            people_to_add = list(Person.objects.filter(pk__in=ids_to_add, is_archived=False))
            if people_to_add:
                event_obj.participants.add(*people_to_add)

        return redirect(next_url)

    def view_attendance_remove_participant_view(self, request, object_id):
        if request.method != "POST":
            return JsonResponse({"status": "error", "message": "Method not allowed."}, status=405)

        event_obj = self.get_object(request, object_id)
        if event_obj is None:
            raise Http404("Event not found.")

        if not self.has_change_permission(request, event_obj):
            raise PermissionDenied

        raw_person_id = (request.POST.get("person_id") or "").strip()
        next_url = (request.POST.get("next") or "").strip()
        fallback_url = reverse("admin:core_event_view_attendance", args=[event_obj.pk])

        if not next_url or not url_has_allowed_host_and_scheme(
            url=next_url,
            allowed_hosts={request.get_host()},
            require_https=request.is_secure(),
        ):
            next_url = fallback_url

        try:
            person_id = int(raw_person_id)
        except (TypeError, ValueError):
            return redirect(next_url)

        person = Person.objects.filter(pk=person_id).first()
        if person is None:
            return redirect(next_url)

        if event_obj.participants.filter(pk=person.pk).exists():
            event_obj.participants.remove(person)

        return redirect(next_url)

    def _write_attendance_sheet(self, worksheet, event_obj, sheet_title, headers, rows, active_filter_summary):
        facilitator_text = ", ".join(str(user) for user in event_obj.facilitators.all()) or "N/A"
        row_index = 1
        worksheet.title = sheet_title
        worksheet.cell(row=row_index, column=1, value=f"Event Attendance - {sheet_title}")
        row_index += 1
        worksheet.cell(row=row_index, column=1, value=f"Event: {event_obj.title}")
        row_index += 1
        worksheet.cell(row=row_index, column=1, value=f"Facilitators: {facilitator_text}")
        row_index += 1
        worksheet.cell(row=row_index, column=1, value=f"Area: {event_obj.area.name if event_obj.area else 'N/A'}")
        row_index += 1
        worksheet.cell(row=row_index, column=1, value=f"Semester: {event_obj.semester.title if event_obj.semester else 'N/A'}")
        row_index += 1
        worksheet.cell(
            row=row_index,
            column=1,
            value=f"Start: {timezone.localtime(event_obj.start_datetime).strftime('%Y-%m-%d %H:%M:%S')}",
        )
        row_index += 1
        worksheet.cell(
            row=row_index,
            column=1,
            value=f"End: {timezone.localtime(event_obj.end_datetime).strftime('%Y-%m-%d %H:%M:%S')}",
        )
        row_index += 1
        worksheet.cell(
            row=row_index,
            column=1,
            value=f"Exported At: {timezone.localtime(timezone.now()).strftime('%Y-%m-%d %H:%M:%S')}",
        )
        row_index += 2

        worksheet.cell(row=row_index, column=1, value="Filters")
        row_index += 1
        if active_filter_summary:
            for filter_label in active_filter_summary:
                worksheet.cell(row=row_index, column=1, value=filter_label)
                row_index += 1
        else:
            worksheet.cell(row=row_index, column=1, value="None")
            row_index += 1

        row_index += 1
        _header_font = Font(bold=True)
        for col_index, header in enumerate(headers, start=1):
            cell = worksheet.cell(row=row_index, column=col_index, value=header)
            cell.font = _header_font
        row_index += 1

        for item in rows:
            for col_index, value in enumerate(item, start=1):
                worksheet.cell(row=row_index, column=col_index, value=value)
            row_index += 1

        return row_index

    def view_attendance_view(self, request, object_id):
        event_obj = self.get_object(request, object_id)
        if event_obj is None:
            raise Http404("Event not found.")

        if not self.has_view_permission(request, event_obj):
            raise PermissionDenied

        snapshot = self._build_attendance_snapshot(event_obj, request.GET)

        export_base_url = reverse("admin:core_event_view_attendance_export_excel", args=[event_obj.pk])
        querystring = request.GET.urlencode()
        export_excel_url = f"{export_base_url}?{querystring}" if querystring else export_base_url

        context = {
            **self.admin_site.each_context(request),
            "opts": self.model._meta,
            "title": f"Attendances for {event_obj.title}",
            "event_obj": event_obj,
            "with_records_count": len(snapshot["with_records_rows"]),
            "no_records_count": len(snapshot["no_records_rows"]),
            "total_persons": len(snapshot["with_records_rows"]) + len(snapshot["no_records_rows"]),
            "back_url": reverse("admin:core_event_changelist"),
            "with_records_view_rows": snapshot["with_records_view_rows"],
            "can_add_participants": self.has_change_permission(request, event_obj),
            "add_participant_url": reverse("admin:core_event_view_attendance_add_participant", args=[event_obj.pk]),
            "add_all_participants_url": reverse("admin:core_event_view_attendance_add_all_participants", args=[event_obj.pk]),
            "remove_participant_url": reverse("admin:core_event_view_attendance_remove_participant", args=[event_obj.pk]),
            "show_non_participants": snapshot["include_non_participants"],
            "addable_with_records_count": sum(
                1
                for row in snapshot["with_records_view_rows"]
                if row.get("can_add") and row.get("person_id")
            ),
            "with_records_table_data": {
                "collapsible": False,
                "headers": ["School ID", "Last Name", "First Name", "First Log", "Last Log", "Device + Area"],
                "rows": snapshot["with_records_rows"],
            },
            "no_records_table_data": {
                "collapsible": False,
                "headers": ["School ID", "Last Name", "First Name", "Reason"],
                "rows": snapshot["no_records_rows"],
            },
            "active_filter_summary": snapshot["active_filter_summary"],
            "filter_device": snapshot["filter_device"],
            "filter_date_from": snapshot["filter_date_from"],
            "filter_date_to": snapshot["filter_date_to"],
            "filter_time_from": snapshot["filter_time_from"],
            "filter_time_to": snapshot["filter_time_to"],
            "filter_search": snapshot["filter_search"],
            "available_devices": snapshot["available_devices"],
            "export_excel_url": export_excel_url,
        }

        return TemplateResponse(request, "admin/core/event/view_attendance.html", context)

    def view_attendance_export_excel_view(self, request, object_id):
        event_obj = self.get_object(request, object_id)
        if event_obj is None:
            raise Http404("Event not found.")

        if not self.has_view_permission(request, event_obj):
            raise PermissionDenied

        snapshot = self._build_attendance_snapshot(event_obj, request.GET)

        workbook = Workbook()
        attendance_sheet = workbook.active
        next_row = self._write_attendance_sheet(
            worksheet=attendance_sheet,
            event_obj=event_obj,
            sheet_title="Attendance",
            headers=["School ID", "Last Name", "First Name", "First Log", "Last Log", "Device + Area"],
            rows=snapshot["with_records_rows"],
            active_filter_summary=snapshot["active_filter_summary"],
        )

        next_row += 1
        attendance_sheet.cell(row=next_row, column=1, value="No Record")
        next_row += 1

        no_record_headers = ["School ID", "Last Name", "First Name", "Reason"]
        _no_rec_header_font = Font(bold=True)
        for col_index, header in enumerate(no_record_headers, start=1):
            cell = attendance_sheet.cell(row=next_row, column=col_index, value=header)
            cell.font = _no_rec_header_font
        next_row += 1

        for item in snapshot["no_records_rows"]:
            for col_index, value in enumerate(item, start=1):
                attendance_sheet.cell(row=next_row, column=col_index, value=value)
            next_row += 1

        response = HttpResponse(
            content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
        )
        safe_title = re.sub(r"[^A-Za-z0-9_-]+", "-", event_obj.title or "event").strip("-") or "event"
        filename = f"{safe_title}-attendance-{timezone.localtime(timezone.now()).strftime('%Y%m%d-%H%M%S')}.xlsx"
        response["Content-Disposition"] = f'attachment; filename="{filename}"'
        workbook.save(response)
        return response
