from datetime import timedelta

from django.conf import settings
from django.db import models
from django.db.models import ExpressionWrapper, F
from django.utils.encoding import python_2_unicode_compatible
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _

EXPIRES_COLUMN_TYPE = models.DateTimeField()
IS_EXPIRED_COLUMN_TYPE = models.BooleanField()


def _get_default_timeout():
    return getattr(settings, "HEALTHCHECKS_DEFAULT_HEARTBEAT_TIMEOUT", timedelta(days=1))


class HeartbeatMonitorQuerySet(models.QuerySet):
    def enabled(self):
        """Filter on enabled checks only."""
        return self.filter(enabled=True)

    def annotate_expires_at(self):
        """Add an ``expires_at`` field to the queryset results."""
        return self.annotate(
            expires_at=ExpressionWrapper(
                (F('last_beat') + F('timeout')),
                output_field=EXPIRES_COLUMN_TYPE
            )
        )

    def expired(self):
        """Tell which services no longer appear to send heartbeats."""
        return self.annotate_expires_at().filter(expires_at__lt=now())

    def expired_names(self):
        """Return a list of all heartbeats names that are expired."""
        return list(self.expired().values_list('name', flat=True))

    def status_by_name(self):
        """Return the expired status for every heartbeat."""
        # Sadly, tests like (F('last_beat') + F('timeout')) < now() aren't supported in Django.
        # Even this fails: .annotate(is_expired=RawSQL("(last_beat + timeout) < %s", [now()]))
        # Thus, have to make the comparison in Python instead.
        t = now()
        monitors = self.annotate_expires_at().values_list('name', 'expires_at')
        return {
            name: (expires_at < t) for name, expires_at in monitors
        }


@python_2_unicode_compatible
class HeartbeatMonitor(models.Model):
    """Monitoring the heartbeat of a task

    When a service is no longer sending out heartbeats, the
    ``check_expired_heartbeats`` check will be triggered.
    """
    name = models.CharField(_("Name"), max_length=200, db_index=True, unique=True)
    enabled = models.BooleanField(_("Enabled"), db_index=True, default=True)
    timeout = models.DurationField(_("Timeout"))
    last_beat = models.DateTimeField(_("Last Beat"), null=True)

    objects = HeartbeatMonitorQuerySet.as_manager()

    def __str__(self):
        return self.name

    class Meta:
        ordering = ('name',)
        verbose_name = _("Heartbeat Monitor")
        verbose_name_plural = _("Heartbeat Monitors")

    @property
    def expires_at(self):
        """Tell when the object will expire"""
        return self.last_beat + self.timeout

    @property
    def is_expired(self):
        """Tell whether the last beat expired."""
        return self.expires_at < now()

    @property
    def remaining_time(self):
        return self.expires_at - now()

    @classmethod
    def _update(cls, name, default_timeout=None, timeout=None):
        """Internal function to update a heartbeat.
        Use :func:`django_healthchecks.heartbeats.update_heartbeat` instead.
        """
        extra_updates = {}
        if timeout is not None:
            extra_updates['timeout'] = timeout

        rows = cls.objects.filter(name=name).update(last_beat=now(), **extra_updates)
        if not rows:
            return cls.objects.create(
                name=name,
                enabled=True,
                timeout=timeout or default_timeout or _get_default_timeout(),
                last_beat=now(),
            )