import datetime

from django.db import models
from django.db.models import Q
from django.utils import timezone as tz
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _

from aircox import utils

from .episode import Episode
from .schedule import Schedule
from .rerun import Rerun, RerunQuerySet


__all__ = ("Diffusion", "DiffusionQuerySet")


class DiffusionQuerySet(RerunQuerySet):
    def episode(self, episode=None, id=None):
        """Diffusions for this episode."""
        return self.filter(episode=episode) if id is None else self.filter(episode__id=id)

    def on_air(self):
        """On air diffusions."""
        return self.filter(type=Diffusion.TYPE_ON_AIR)

    # TODO: rename to `datetime`
    def now(self, now=None, order=True):
        """Diffusions occuring now."""
        now = now or tz.now()
        qs = self.filter(start__lte=now, end__gte=now).distinct()
        return qs.order_by("start") if order else qs

    def date(self, date=None, order=True):
        """Diffusions occuring date."""
        date = date or datetime.date.today()
        start = tz.make_aware(tz.datetime.combine(date, datetime.time()))
        end = tz.make_aware(tz.datetime.combine(date, datetime.time(23, 59, 59, 999)))
        # start = tz.get_current_timezone().localize(start)
        # end = tz.get_current_timezone().localize(end)
        qs = self.filter(start__range=(start, end))
        return qs.order_by("start") if order else qs

    def at(self, date, order=True):
        """Return diffusions at specified date or datetime."""
        return self.now(date, order) if isinstance(date, tz.datetime) else self.date(date, order)

    def after(self, date=None):
        """Return a queryset of diffusions that happen after the given date
        (default: today)."""
        date = utils.date_or_default(date)
        if isinstance(date, tz.datetime):
            qs = self.filter(Q(start__gte=date) | Q(end__gte=date))
        else:
            qs = self.filter(Q(start__date__gte=date) | Q(end__date__gte=date))
        return qs.order_by("start")

    def before(self, date=None):
        """Return a queryset of diffusions that finish before the given date
        (default: today)."""
        date = utils.date_or_default(date)
        if isinstance(date, tz.datetime):
            qs = self.filter(start__lt=date)
        else:
            qs = self.filter(start__date__lt=date)
        return qs.order_by("start")

    def range(self, start, end):
        # FIXME can return dates that are out of range...
        return self.after(start).before(end)


class Diffusion(Rerun):
    """A Diffusion is an occurrence of a Program that is scheduled on the
    station's timetable. It can be a rerun of a previous diffusion. In such a
    case, use rerun's info instead of its own.

    A Diffusion without any rerun is named Episode (previously, a
    Diffusion was different from an Episode, but in the end, an
    episode only has a name, a linked program, and a list of sounds, so we
    finally merge theme).

    A Diffusion can have different types:
    - default: simple diffusion that is planified / did occurred
    - unconfirmed: a generated diffusion that has not been confirmed and thus
        is not yet planified
    - cancel: the diffusion has been canceled
    - stop: the diffusion has been manually stopped
    """

    objects = DiffusionQuerySet.as_manager()

    TYPE_ON_AIR = 0x00
    TYPE_UNCONFIRMED = 0x01
    TYPE_CANCEL = 0x02
    TYPE_CHOICES = (
        (TYPE_ON_AIR, _("on air")),
        (TYPE_UNCONFIRMED, _("not confirmed")),
        (TYPE_CANCEL, _("cancelled")),
    )

    episode = models.ForeignKey(
        Episode,
        models.CASCADE,
        verbose_name=_("episode"),
    )
    schedule = models.ForeignKey(
        Schedule,
        models.CASCADE,
        verbose_name=_("schedule"),
        blank=True,
        null=True,
    )
    type = models.SmallIntegerField(
        verbose_name=_("type"),
        default=TYPE_ON_AIR,
        choices=TYPE_CHOICES,
    )
    start = models.DateTimeField(_("start"), db_index=True)
    end = models.DateTimeField(_("end"), db_index=True)
    # port = models.ForeignKey(
    #    'self',
    #    verbose_name = _('port'),
    #    blank = True, null = True,
    #    on_delete=models.SET_NULL,
    #    help_text = _('use this input port'),
    # )

    item_template_name = "aircox/widgets/diffusion_item.html"

    class Meta:
        verbose_name = _("Diffusion")
        verbose_name_plural = _("Diffusions")
        permissions = (("programming", _("edit the diffusions' planification")),)

    def __str__(self):
        str_ = "{episode} - {date}".format(
            episode=self.episode and self.episode.title,
            date=self.local_start.strftime("%Y/%m/%d %H:%M%z"),
        )
        if self.initial:
            str_ += " ({})".format(_("rerun"))
        return str_

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        if self.is_initial and self.episode != self._initial["episode"]:
            self.rerun_set.update(episode=self.episode, program=self.program)

    # def save(self, no_check=False, *args, **kwargs):
    # if self.start != self._initial['start'] or \
    #        self.end != self._initial['end']:
    #    self.check_conflicts()

    def save_rerun(self):
        self.episode = self.initial.episode
        super().save_rerun()

    def save_initial(self):
        self.program = self.episode.program

    @property
    def duration(self):
        return self.end - self.start

    @property
    def date(self):
        """Return diffusion start as a date."""

        return utils.cast_date(self.start)

    @cached_property
    def local_start(self):
        """Return a version of self.date that is localized to self.timezone;
        This is needed since datetime are stored as UTC date and we want to get
        it as local time."""

        return tz.localtime(self.start, tz.get_current_timezone())

    @property
    def local_end(self):
        """Return a version of self.date that is localized to self.timezone;
        This is needed since datetime are stored as UTC date and we want to get
        it as local time."""

        return tz.localtime(self.end, tz.get_current_timezone())

    @property
    def is_now(self):
        """True if diffusion is currently running."""
        now = tz.now()
        return self.type == self.TYPE_ON_AIR and self.start <= now and self.end >= now

    @property
    def is_live(self):
        """True if Diffusion is live (False if there are sounds files)."""
        return self.type == self.TYPE_ON_AIR and not self.episode.sound_set.archive().count()

    def get_playlist(self, **types):
        """Returns sounds as a playlist (list of *local* archive file path).

        The given arguments are passed to ``get_sounds``.
        """
        from .sound import Sound

        return list(
            self.get_sounds(**types).filter(path__isnull=False, type=Sound.TYPE_ARCHIVE).values_list("path", flat=True)
        )

    def get_sounds(self, **types):
        """Return a queryset of sounds related to this diffusion, ordered by
        type then path.

        **types: filter on the given sound types name, as `archive=True`
        """
        from .sound import Sound

        sounds = (self.initial or self).sound_set.order_by("type", "path")
        _in = [getattr(Sound.Type, name) for name, value in types.items() if value]

        return sounds.filter(type__in=_in)

    def is_date_in_range(self, date=None):
        """Return true if the given date is in the diffusion's start-end
        range."""
        date = date or tz.now()

        return self.start < date < self.end

    def get_conflicts(self):
        """Return conflicting diffusions queryset."""

        # conflicts=Diffusion.objects.filter(
        #    Q(start__lt=OuterRef('start'), end__gt=OuterRef('end')) |
        #    Q(start__gt=OuterRef('start'), start__lt=OuterRef('end'))
        # )
        # diffs= Diffusion.objects.annotate(conflict_with=Exists(conflicts))
        #                         .filter(conflict_with=True)
        return (
            Diffusion.objects.filter(
                Q(start__lt=self.start, end__gt=self.start) | Q(start__gt=self.start, start__lt=self.end)
            )
            .exclude(pk=self.pk)
            .distinct()
        )

    def check_conflicts(self):
        conflicts = self.get_conflicts()
        self.conflicts.set(conflicts)

    _initial = None

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._initial = {
            "start": self.start,
            "end": self.end,
            "episode": getattr(self, "episode", None),
        }
