from django.db import models
from django.conf import settings
from django.db.models import Q
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from dateutil.relativedelta import relativedelta


def define_all(QuerySet, Model):

    class TrackedLiveQuerySet(QuerySet):
        """
        Takes advantage from the created_on and updated_on fields to perform many additional queries.
        """

        @classmethod
        def _quarter_month(cls, month):
            """
            For a given month, returns the corresponding month starting its quarter (1, 4, 7, 10)
            :param month: The current month
            :return: The quarter month
            """
            if month > 9:
                return 10
            if month > 6:
                return 7
            if month > 3:
                return 4
            return 1

        @classmethod
        def _semester_month(cls, month):
            """
            For a given month, returns the corresponding month starting its semester (1, 7)
            :param month: The current month
            :return: The semester month
            """
            if month > 6:
                return 7
            return 1

        @classmethod
        def _period_date(cls, period):
            """
            Computes the period to filter (d=1-day, w=1-week, m=1-month, q=3-months, h=6-months, y=1-year).
            Additional options: D=today, M=this-month, Q=this-quarter, H=this-semester, Y=this-year
            Returns an interval as a tuple (start, end) where the end date is a result of now() and the
              start date is a result of now() - (days or months before).
            :param period: A single-character value in [dwmqhy]. Any other value will raise a KeyError.
            :raises: KeyError if the period is not valid.
            :returns: a tuple (start, end) of datetime objects.
            """

            now_ = now()
            return {
                'd': now_ - relativedelta(days=1),
                'w': now_ - relativedelta(weeks=1),
                'm': now_ - relativedelta(months=1),
                'q': now_ - relativedelta(months=3),
                'h': now_ - relativedelta(months=6),
                'y': now_ - relativedelta(years=1),
                'D': now_.replace(hour=0, minute=0, second=0, microsecond=0),
                'M': now_.replace(hour=0, minute=0, second=0, microsecond=0, day=1),
                'Q': now_.replace(hour=0, minute=0, second=0, microsecond=0, day=1, month=cls._quarter_month(now_.month)),
                'H': now_.replace(hour=0, minute=0, second=0, microsecond=0, day=1, month=cls._semester_month(now_.month)),
                'Y': now_.replace(hour=0, minute=0, second=0, microsecond=0, day=1, month=1),
            }[period], now_

        def created_on(self, period):
            """
            Filters by objects being created on certain periods (d, w, m, h, y, D, M, Q, H, Y).
            Read more in _period_date(period) documentation about period meaning.
            :param period: A single-character value in [dwmqhyDMQHY]. Any other value will raise a KeyError.
            :raises: KeyError if the period is not valid.
            :returns: A queryset, filtered with creation date between now and a specific period ago.
            """

            return self.filter(created_on__range=self._period_date(period))

        def updated_on(self, period):
            """
            Filters by objects being updated on certain periods (d, w, m, h, y, D, M, Q, H, Y).
            Read more in _period_date(period) documentation about period meaning.
            :param period: A single-character value in [dwmqhyDMQHY]. Any other value will raise a KeyError.
            :raises: KeyError if the period is not valid.
            :returns: A queryset, filtered with update date between now and a specific period ago.
            """

            return self.filter(updated_on__range=self._period_date(period))

        def created_or_updated_on(self, period):
            """
            Filters by a union between created_on and updated_on.
            :param period: A single-character value in [dwmqhyDMQHY]. Any other value will raise a KeyError.
            :raises: KeyError if the period is not valid.
            :returns: A queryset, filtered with update date between now and a specific period ago.
            """

            period_date = self._period_date(period)
            return self.filter(Q(created_on__range=period_date)|Q(updated_on__range=period_date))

    class TrackedLiveAndDeadQuerySet(TrackedLiveQuerySet):
        """
        Takes advantage of the deleted_on field to perform an additional implicit filter on all().
        With this filter on, elements being deleted will not be visible anymore by queryset.
        """

        def all(self, seriously=False):
            """
            Ask by .all() also filters by having deleted_on in null.
            If seriously, then the former behavior is called instead.
            """

            qs = super(TrackedLiveAndDeadQuerySet, self).all()
            return qs if seriously else qs.filter(deleted_on__isnull=True)

        def delete(self, seriously=False):
            """
            Marks elements as deleted.
            If seriously, then the former behavior is called instead.
            """

            return super(TrackedLiveAndDeadQuerySet, self).delete() if seriously else self.update(deleted_on=now())

        def dead(self):
            """
            Lists the dead elements. It is the opposite of all([seriously=False]).
            """

            return super(TrackedLiveAndDeadQuerySet, self).filter(deleted_on__isnull=False)

    class TrackedLive(Model):
        """
        Models are tracked by date. It holds create and update dates.
        """

        created_on = models.DateTimeField(default=now, null=False, editable=False, verbose_name=_(u"Creation Date"),
                                          help_text=_(u"Date and time of record creation"))
        updated_on = models.DateTimeField(default=now, null=False, editable=False, verbose_name=_(u"Update Date"),
                                          help_text=_(u"Date and time of last record update"))

        objects = TrackedLiveQuerySet.as_manager()

        class Meta:
            abstract = True

        def save(self, force_insert=False, force_update=False, using=None,
                 update_fields=None):
            """
            Overrides usual save() behavior to also update (unless explicitly commanded to not do so)
              the updated_on date.
            :param force_insert:
            :param force_update:
            :param using:
            :param update_fields:
            :return:
            """

            if not update_fields or '-updated_on' not in update_fields:
                self.updated_on = now()
            else:
                index = update_fields.index('-updated_on')
                update_fields = update_fields[:index] + update_fields[index+1:]
            return super(TrackedLive, self).save(force_insert=force_insert, force_update=force_update, using=using,
                                                 update_fields=update_fields)

    class TrackedLiveAndDead(TrackedLive):
        """
        This class allows logical deletion instead of physical. It does so by adding a deletion date field.
        Since it extends the TrackedLive behavior, it also has those fields.
        """

        deleted_on = models.DateTimeField(null=True, editable=False, verbose_name=_(u"Deletion Date"),
                                          help_text=_(u"Date and time of record deletion"))

        objects = TrackedLiveAndDeadQuerySet.as_manager()

        class Meta:
            abstract = True

        def delete(self, using=None, seriously=False):
            """
            Deletes an object, but logically (not from database).
            :param using:
            :return:
            """

            if seriously:
                return super(TrackedLiveAndDead, self).delete(using=using)
            else:
                self.deleted_on = now()
                return self.save(using=using, update_fields=('deleted_on', '-updated_on'))

    class TrackedUserCreated(Model):
        """
        This class allows specifying a creator for an object.
        """

        created_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, editable=False, related_name="+",
                                       on_delete=models.DO_NOTHING, verbose_name=_(u"Created By"),
                                       help_text=_(u"User marked as creator of this record"))

        class Meta:
            abstract = True

    class TrackedUserOwned(Model):
        """
        This class allows specifying an owner for an object.
        """

        owned_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=False, related_name="+",
                                     on_delete=models.DO_NOTHING, verbose_name=_(u"Owned By"),
                                     help_text=_(u"User marked as owner of this record"))

        class Meta:
            abstract = True

    return (TrackedLive, TrackedLiveAndDead, TrackedLiveQuerySet, TrackedLiveAndDeadQuerySet,
            TrackedUserCreated, TrackedUserOwned)
