from django import forms
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import transaction
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView
from wagtail.admin.views.generic.base import WagtailAdminTemplateMixin
from wagtail.models import ReferenceIndex, Revision

from dnscms.utils import slugify
from events.models import EventOrganizer, EventOrganizerLink, EventPage
from events.views import event_organizer_chooser_viewset


def _check_relations_are_handled():
    """Refuse to merge if EventOrganizer has gained a relation this module doesn't handle."""
    for relation in EventOrganizer._meta.related_objects:
        if relation.many_to_many and relation.through is EventOrganizerLink:
            continue
        if relation.related_model is EventOrganizerLink and relation.field.name == "organizer":
            continue
        raise NotImplementedError(
            f"merge_event_organizers does not handle the {relation.name!r} relation"
        )


def _repoint_links(survivor, loser):
    loser_links = EventOrganizerLink.objects.filter(organizer=loser)
    affected_event_ids = set(loser_links.values_list("event_id", flat=True))
    # drop the loser's link where the event links both, to respect the unique constraint
    doubly_linked = EventOrganizerLink.objects.filter(
        organizer=survivor, event_id__in=affected_event_ids
    ).values_list("event_id", flat=True)
    loser_links.filter(event_id__in=doubly_linked).delete()
    loser_links.update(organizer=survivor)
    return affected_event_ids


def _rewrite_revisions(survivor, loser):
    """Rewrite organizer ids in revision JSON, which would otherwise dangle after the delete."""
    affected_event_ids = set()
    event_content_type = ContentType.objects.get_for_model(EventPage)
    for revision in Revision.objects.filter(content_type=event_content_type).iterator():
        links = revision.content.get("organizer_links") or []
        if not any(link.get("organizer") == loser.pk for link in links):
            continue
        seen_organizer_ids = set()
        rewritten = []
        for link in links:
            if link.get("organizer") == loser.pk:
                link = {**link, "organizer": survivor.pk}
            if link.get("organizer") in seen_organizer_ids:
                continue
            seen_organizer_ids.add(link.get("organizer"))
            rewritten.append(link)
        revision.content["organizer_links"] = rewritten
        revision.save(update_fields=["content"])
        affected_event_ids.add(int(revision.object_id))
    return affected_event_ids


@transaction.atomic
def merge_event_organizers(organizer_a, organizer_b, *, name=None, slug=None):
    """
    Merge two organizers, keeping the one with the lowest primary key. Event
    links and event page revisions are repointed before the other organizer is
    deleted. ``name``/``slug`` override the survivor's fields; a blank
    ``association``/``external_url`` is filled in from the deleted organizer.
    """
    if organizer_a.pk == organizer_b.pk:
        raise ValueError("Cannot merge an organizer with itself.")
    _check_relations_are_handled()

    survivor, loser = sorted([organizer_a, organizer_b], key=lambda organizer: organizer.pk)

    affected_event_ids = _repoint_links(survivor, loser)
    affected_event_ids |= _rewrite_revisions(survivor, loser)

    if name:
        survivor.name = name
    if slug:
        survivor.slug = slug
    if not survivor.association_id:
        survivor.association_id = loser.association_id
    if not survivor.external_url:
        survivor.external_url = loser.external_url
    survivor.save()

    loser.delete()

    # the bulk link update bypasses the save-time reference index maintenance
    for event in EventPage.objects.filter(pk__in=affected_event_ids):
        ReferenceIndex.create_or_update_for_object(event)

    return survivor


CUSTOM_VALUE = "__custom__"


class MergeOrganizersSelectForm(forms.Form):
    organizer_a = forms.ModelChoiceField(
        label=_("First organizer"),
        queryset=EventOrganizer.objects.all(),
        widget=event_organizer_chooser_viewset.widget_class(),
    )
    organizer_b = forms.ModelChoiceField(
        label=_("Second organizer"),
        queryset=EventOrganizer.objects.all(),
        widget=event_organizer_chooser_viewset.widget_class(),
    )

    def clean(self):
        cleaned = super().clean()
        organizer_a = cleaned.get("organizer_a")
        organizer_b = cleaned.get("organizer_b")
        if organizer_a and organizer_b and organizer_a.pk == organizer_b.pk:
            raise ValidationError(_("Pick two different organizers."))
        return cleaned


class MergeOrganizersConfirmForm(forms.Form):
    name = forms.ChoiceField(label=_("Name to keep"), widget=forms.RadioSelect)
    name_custom = forms.CharField(label=_("Custom name"), required=False, max_length=100)
    slug = forms.ChoiceField(label=_("Slug to keep"), widget=forms.RadioSelect)
    slug_custom = forms.CharField(label=_("Custom slug"), required=False, max_length=255)

    def __init__(self, *args, survivor, loser, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields["name"].choices = self._choices(survivor.name, loser.name)
        self.fields["name"].initial = survivor.name
        self.fields["slug"].choices = self._choices(survivor.slug, loser.slug)
        self.fields["slug"].initial = survivor.slug

    @staticmethod
    def _choices(*values):
        unique_values = dict.fromkeys(values)
        return [(value, value) for value in unique_values] + [
            (CUSTOM_VALUE, _("Something else (enter below)"))
        ]

    def _resolve(self, cleaned, field):
        choice = cleaned.get(field)
        custom = (cleaned.get(f"{field}_custom") or "").strip()
        if choice == CUSTOM_VALUE:
            if not custom:
                self.add_error(
                    f"{field}_custom", _("Enter a value, or pick an existing one above.")
                )
            return custom
        return choice

    def clean(self):
        cleaned = super().clean()
        cleaned["final_name"] = self._resolve(cleaned, "name")
        cleaned["final_slug"] = slugify(self._resolve(cleaned, "slug") or "")
        return cleaned


class MergeOrganizersSelectView(WagtailAdminTemplateMixin, FormView):
    template_name = "events/merge_organizers_select.html"
    form_class = MergeOrganizersSelectForm
    page_title = _("Merge organizers")
    header_icon = "group"

    def form_valid(self, form):
        params = urlencode(
            {
                "organizer_a": form.cleaned_data["organizer_a"].pk,
                "organizer_b": form.cleaned_data["organizer_b"].pk,
            }
        )
        return redirect(f"{reverse('events_merge_organizers_confirm')}?{params}")


class MergeOrganizersConfirmView(WagtailAdminTemplateMixin, FormView):
    template_name = "events/merge_organizers_confirm.html"
    form_class = MergeOrganizersConfirmForm
    page_title = _("Merge organizers")
    header_icon = "group"

    def dispatch(self, request, *args, **kwargs):
        try:
            pks = {int(request.GET.get(param, "")) for param in ("organizer_a", "organizer_b")}
        except ValueError:
            pks = set()
        organizers = list(EventOrganizer.objects.filter(pk__in=pks).order_by("pk"))
        if len(organizers) != 2:
            messages.error(request, _("Pick two different organizers to merge."))
            return redirect("events_merge_organizers")
        self.survivor, self.loser = organizers
        return super().dispatch(request, *args, **kwargs)

    def get_form_kwargs(self):
        return {**super().get_form_kwargs(), "survivor": self.survivor, "loser": self.loser}

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["survivor"] = self.survivor
        context["loser"] = self.loser
        context["survivor_event_count"] = self.survivor.organized_events.count()
        context["loser_event_count"] = self.loser.organized_events.count()
        return context

    def form_valid(self, form):
        survivor = merge_event_organizers(
            self.survivor,
            self.loser,
            name=form.cleaned_data["final_name"],
            slug=form.cleaned_data["final_slug"],
        )
        messages.success(
            self.request,
            _('Merged "%(loser)s" into "%(survivor)s".')
            % {"loser": self.loser.name, "survivor": survivor.name},
        )
        return redirect(reverse("wagtailsnippets_events_eventorganizer:list"))
