from django.contrib.auth.models import User
from django.contrib.postgres.constraints import ExclusionConstraint
from django.contrib.postgres.fields import DateRangeField
from django.contrib.postgres.fields import RangeOperators
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext as _

from utils.mixins import CreatedModifiedAbstract


class Member(User):
    class QuerySet(models.QuerySet):
        def annotate_membership(self):
            from .selectors import get_current_subscription_period

            current_subscription_period = get_current_subscription_period()

            if not current_subscription_period:
                raise ValueError("No current subscription period found")

            return self.annotate(
                active_membership=models.Exists(
                    Membership.objects.filter(
                        user=models.OuterRef("pk"),
                        period=current_subscription_period.id,
                    ),
                ),
            )

    objects = QuerySet.as_manager()

    class Meta:
        proxy = True


class SubscriptionPeriod(CreatedModifiedAbstract):
    """
    Denotes a period for which members should pay their membership fee for.
    """

    period = DateRangeField(verbose_name=_("period"))

    class Meta:
        constraints = [
            ExclusionConstraint(
                name="exclude_overlapping_periods",
                expressions=[
                    ("period", RangeOperators.OVERLAPS),
                ],
            ),
        ]

    def __str__(self):
        return (
            f"{self.period.lower} - {self.period.upper or _('next general assembly')}"
        )


class Membership(CreatedModifiedAbstract):
    """
    Tracks that a user has membership of a given type for a given period.
    """

    class QuerySet(models.QuerySet):
        def for_member(self, member: Member):
            return self.filter(user=member)

        def _current(self):
            return self.filter(period__period__contains=timezone.now())

        def current(self) -> "Membership | None":
            try:
                return self._current().get()
            except self.model.DoesNotExist:
                return None

        def previous(self) -> list["Membership"]:
            # A naïve way to get previous by just excluding the current. This
            # means that there must be some protection against "future"
            # memberships.
            return list(self.all().difference(self._current()))

    objects = QuerySet.as_manager()

    class Meta:
        verbose_name = _("membership")
        verbose_name_plural = _("memberships")

    user = models.ForeignKey("auth.User", on_delete=models.PROTECT)

    membership_type = models.ForeignKey(
        "membership.MembershipType",
        related_name="memberships",
        verbose_name=_("subscription type"),
        on_delete=models.PROTECT,
    )

    period = models.ForeignKey(
        "membership.SubscriptionPeriod",
        on_delete=models.PROTECT,
    )

    def __str__(self):
        return f"{self.user} - {self.period}"


class MembershipType(CreatedModifiedAbstract):
    """
    Models membership types. Currently only a name, but will in the future
    possibly contain more information like fees.
    """

    class Meta:
        verbose_name = _("membership type")
        verbose_name_plural = _("membership types")

    name = models.CharField(verbose_name=_("name"), max_length=64)

    def __str__(self):
        return self.name
