#
# steering, Programme/schedule management for AURA
#
# Copyright (C) 2011-2017, 2020, Ernesto Rico Schmidt
# Copyright (C) 2017-2019, Ingo Leindecker
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

from datetime import datetime

from rest_framework.exceptions import ValidationError
from versatileimagefield.fields import PPOIField, VersatileImageField

from django.contrib.auth.models import User
from django.db import models
from django.db.models import Q, QuerySet
from django.utils.translation import gettext_lazy as _
from program.utils import parse_datetime
from steering.settings import THUMBNAIL_SIZES


class ScheduleConflictError(ValidationError):
    def __init__(self, *args, conflicts=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.conflicts = conflicts


class Type(models.Model):
    is_active = models.BooleanField(default=True)
    name = models.CharField(max_length=32)
    slug = models.SlugField(max_length=32, unique=True)

    class Meta:
        ordering = ("name",)

    def __str__(self):
        return self.name


class Category(models.Model):
    description = models.TextField(blank=True)
    is_active = models.BooleanField(default=True)
    name = models.CharField(max_length=32)
    slug = models.SlugField(max_length=32, unique=True)
    subtitle = models.CharField(blank=True, max_length=32)

    class Meta:
        ordering = ("name",)
        verbose_name_plural = "Categories"

    def __str__(self):
        return self.name


class Topic(models.Model):
    is_active = models.BooleanField(default=True)
    name = models.CharField(max_length=32)
    slug = models.SlugField(max_length=32, unique=True)

    class Meta:
        ordering = ("name",)

    def __str__(self):
        return self.name


class MusicFocus(models.Model):
    is_active = models.BooleanField(default=True)
    name = models.CharField(max_length=32)
    slug = models.SlugField(max_length=32, unique=True)

    class Meta:
        ordering = ("name",)
        verbose_name_plural = "Music Focus"

    def __str__(self):
        return self.name


class FundingCategory(models.Model):
    is_active = models.BooleanField(default=True)
    name = models.CharField(max_length=32)
    slug = models.SlugField(max_length=32, unique=True)

    class Meta:
        ordering = ("name",)
        verbose_name_plural = "Funding Categories"

    def __str__(self):
        return self.name


class Language(models.Model):
    is_active = models.BooleanField(default=True)
    name = models.CharField(max_length=32)

    class Meta:
        ordering = ("name",)

    def __str__(self):
        return self.name


class License(models.Model):
    identifier = models.CharField(max_length=32, help_text="Identifier of the license")
    name = models.CharField(max_length=64, help_text="Name of the license")
    needs_author = models.BooleanField(default=True)
    requires_express_permission_for_publication = models.BooleanField(default=True)
    url = models.URLField(default="", blank=True)

    class Meta:
        ordering = ("name",)

    def __str__(self):
        return self.identifier


class Image(models.Model):
    alt_text = models.TextField(blank=True, default="")
    credits = models.TextField(blank=True, default="")
    is_use_explicitly_granted_by_author = models.BooleanField(default=False)
    height = models.PositiveIntegerField(blank=True, null=True)
    image = VersatileImageField(
        blank=True,
        height_field="height",
        null=True,
        ppoi_field="ppoi",
        upload_to="images",
        width_field="width",
    )
    license = models.ForeignKey(
        License, null=True, on_delete=models.SET_NULL, related_name="images"
    )
    owner = models.CharField(max_length=150)
    ppoi = PPOIField()
    width = models.PositiveIntegerField(blank=True, null=True)

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

        if self.image.name and THUMBNAIL_SIZES:
            for size in THUMBNAIL_SIZES:
                self.image.thumbnail = self.image.crop[size].name

    def delete(self, using=None, keep_parents=False):
        self.image.delete_all_created_images()
        self.image.delete(save=False)

        super().delete(using, keep_parents)


class Host(models.Model):
    biography = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    created_by = models.CharField(max_length=150)
    email = models.EmailField(blank=True)
    image = models.ForeignKey(Image, null=True, on_delete=models.CASCADE, related_name="hosts")
    is_active = models.BooleanField(default=True)
    name = models.CharField(max_length=128)
    owners = models.ManyToManyField(User, blank=True, related_name="hosts")
    updated_at = models.DateTimeField(auto_now=True, blank=True, null=True)
    updated_by = models.CharField(blank=True, default="", max_length=150)

    class Meta:
        ordering = ("name",)

    def __str__(self):
        return self.name


class LinkType(models.Model):
    name = models.CharField(max_length=32, help_text="Name of the link type")
    type = models.CharField(max_length=32, help_text="Type of the link")

    class Meta:
        ordering = ("name",)

    def __str__(self):
        return self.type


class Link(models.Model):
    type = models.CharField(max_length=64)
    url = models.URLField()

    class Meta:
        abstract = True

    def __str__(self):
        return self.url


class HostLink(Link):
    host = models.ForeignKey(Host, on_delete=models.CASCADE, related_name="links")


class Show(models.Model):
    category = models.ManyToManyField(Category, blank=True, related_name="shows")
    cba_series_id = models.IntegerField(blank=True, null=True)
    created_at = models.DateTimeField(auto_now_add=True)
    created_by = models.CharField(max_length=150)
    default_playlist_id = models.IntegerField(blank=True, null=True)
    description = models.TextField(blank=True)
    email = models.EmailField(blank=True, null=True)
    funding_category = models.ForeignKey(
        FundingCategory, blank=True, null=True, on_delete=models.CASCADE, related_name="shows"
    )
    hosts = models.ManyToManyField(Host, blank=True, related_name="shows")
    image = models.ForeignKey(Image, null=True, on_delete=models.CASCADE, related_name="shows")
    internal_note = models.TextField(blank=True)
    is_active = models.BooleanField(default=True)
    is_public = models.BooleanField(default=False)
    language = models.ManyToManyField(Language, blank=True, related_name="shows")
    # TODO: is this really necessary?
    logo = models.ForeignKey(
        Image, blank=True, null=True, on_delete=models.CASCADE, related_name="logo_shows"
    )
    music_focus = models.ManyToManyField(MusicFocus, blank=True, related_name="shows")
    name = models.CharField(max_length=255)
    owners = models.ManyToManyField(User, blank=True, related_name="shows")
    predecessor = models.ForeignKey(
        "self", blank=True, null=True, on_delete=models.CASCADE, related_name="successors"
    )
    short_description = models.TextField()
    slug = models.CharField(max_length=255, unique=True)
    topic = models.ManyToManyField(Topic, blank=True, related_name="shows")
    type = models.ForeignKey(
        Type, blank=True, null=True, on_delete=models.CASCADE, related_name="shows"
    )
    updated_at = models.DateTimeField(auto_now=True, blank=True, null=True)
    updated_by = models.CharField(blank=True, default="", max_length=150)

    class Meta:
        ordering = ("slug",)

    def __str__(self):
        return self.name


class ShowLink(Link):
    show = models.ForeignKey(Show, on_delete=models.CASCADE, related_name="links")


class RRule(models.Model):
    by_set_pos = models.IntegerField(
        blank=True,
        choices=[
            (1, "first"),
            (2, "second"),
            (3, "third"),
            (4, "fourth"),
            (5, "fifth"),
            (-1, "last"),
        ],
        null=True,
    )
    by_weekdays = models.CharField(
        blank=True,
        choices=[
            (None, ""),
            ("0,1,2,3,4", "business days"),
            ("5,6", "weekends"),
        ],
        null=True,
        max_length=9,
    )
    count = models.IntegerField(
        blank=True,
        null=True,
        help_text="How many occurrences should be generated.",
    )
    freq = models.IntegerField(
        choices=[
            (0, "once"),
            (1, "monthly"),
            (2, "weekly"),
            (3, "daily"),
        ]
    )
    interval = models.IntegerField(
        default=1,
        help_text="The interval between each freq iteration.",
    )
    name = models.CharField(max_length=32, unique=True)

    class Meta:
        ordering = ("pk",)
        unique_together = ("freq", "interval", "by_set_pos", "by_weekdays")
        verbose_name = _("recurrence rule")

    def __str__(self):
        return self.name


class Schedule(models.Model):
    add_business_days_only = models.BooleanField(
        default=False,
        help_text=(
            "Whether to add add_days_no but skipping the weekends. "
            "E.g. if weekday is Friday, the date returned will be the next Monday."
        ),
    )
    add_days_no = models.IntegerField(
        blank=True,
        null=True,
        help_text=(
            "Add a number of days to the generated dates. "
            "This can be useful for repetitions, like 'On the following day'."
        ),
    )
    by_weekday = models.IntegerField(
        help_text="Number of the Weekday.",
        choices=[
            (0, "Monday"),
            (1, "Tuesday"),
            (2, "Wednesday"),
            (3, "Thursday"),
            (4, "Friday"),
            (5, "Saturday"),
            (6, "Sunday"),
        ],
        null=True,
    )
    default_playlist_id = models.IntegerField(
        blank=True,
        null=True,
        help_text="A tank ID in case the timeslot's playlist_id is empty.",
    )
    end_time = models.TimeField(null=True, help_text="End time of schedule.")
    first_date = models.DateField(help_text="Start date of schedule.")
    is_repetition = models.BooleanField(
        default=False,
        help_text="Whether the schedule is a repetition.",
    )
    last_date = models.DateField(help_text="End date of schedule.", null=True)
    rrule = models.ForeignKey(
        RRule, help_text="A recurrence rule.", on_delete=models.CASCADE, related_name="schedules"
    )
    show = models.ForeignKey(
        Show,
        help_text="Show the schedule belongs to.",
        on_delete=models.CASCADE,
        related_name="schedules",
    )
    start_time = models.TimeField(help_text="Start time of schedule.")

    class Meta:
        ordering = ("first_date", "start_time")


class TimeSlotManager(models.Manager):
    @staticmethod
    def instantiate(start, end, schedule):
        return TimeSlot(
            start=parse_datetime(start),
            end=parse_datetime(end),
            schedule=schedule,
        )

    @staticmethod
    def get_timerange_timeslots(start_timerange: datetime, end_timerange: datetime) -> QuerySet:
        """get the timeslots between start_timerange and end_timerange"""

        return TimeSlot.objects.filter(
            # start before start_timerange, end after start_timerange
            Q(start__lt=start_timerange, end__gt=start_timerange)
            # start after/at start_timerange, end before/at end_timerange
            | Q(start__gte=start_timerange, end__lte=end_timerange)
            # start before end_timerange, end after/at end_timerange
            | Q(start__lt=end_timerange, end__gte=end_timerange)
        )

    @staticmethod
    def get_colliding_timeslots(timeslot):
        return TimeSlot.objects.filter(
            (Q(start__lt=timeslot.end) & Q(end__gte=timeslot.end))
            | (Q(end__gt=timeslot.start) & Q(end__lte=timeslot.end))
            | (Q(start__gte=timeslot.start) & Q(end__lte=timeslot.end))
            | (Q(start__lte=timeslot.start) & Q(end__gte=timeslot.end))
        )


class TimeSlot(models.Model):
    end = models.DateTimeField()
    language = models.ManyToManyField(Language, blank=True, related_name="timeslots")
    memo = models.TextField(blank=True)
    playlist_id = models.IntegerField(null=True)
    repetition_of = models.ForeignKey(
        "self", blank=True, null=True, on_delete=models.CASCADE, related_name="repetitions"
    )
    schedule = models.ForeignKey(Schedule, on_delete=models.CASCADE, related_name="timeslots")
    start = models.DateTimeField()
    topic = models.ManyToManyField(Topic, blank=True, related_name="timeslots")

    objects = TimeSlotManager()

    class Meta:
        ordering = ("start", "end")

    def __str__(self):
        if self.start.date() == self.end.date():
            time_span = "{0}, {1} - {2}".format(
                self.start.strftime("%x"),
                self.start.strftime("%X"),
                self.end.strftime("%X"),
            )
        else:
            time_span = "{0} - {1}".format(
                self.start.strftime("%X %x"),
                self.end.strftime("%X %x"),
            )

        return f"{str(self.schedule.show)} ({time_span})"

    @property
    def hash(self):
        string = (
            str(self.start)
            + str(self.end)
            + str(self.schedule.rrule.id)
            + str(self.schedule.by_weekday)
        )
        return str("".join(s for s in string if s.isdigit()))


class Note(models.Model):
    cba_id = models.IntegerField(blank=True, null=True)
    content = models.TextField()
    contributors = models.ManyToManyField(Host, related_name="notes")
    created_at = models.DateTimeField(auto_now_add=True)
    created_by = models.CharField(max_length=150)
    image = models.ForeignKey(Image, null=True, on_delete=models.CASCADE, related_name="notes")
    owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="notes", default=1)
    playlist = models.TextField(blank=True)
    slug = models.SlugField(max_length=32, unique=True)
    summary = models.TextField(blank=True)
    tags = models.TextField(blank=True)
    timeslot = models.OneToOneField(TimeSlot, null=True, on_delete=models.SET_NULL, unique=True)
    title = models.CharField(max_length=128)
    updated_at = models.DateTimeField(auto_now=True, blank=True, null=True)
    updated_by = models.CharField(blank=True, default="", max_length=150)

    class Meta:
        ordering = ("timeslot",)

    def __str__(self):
        return self.title


class NoteLink(Link):
    note = models.ForeignKey(Note, on_delete=models.CASCADE, related_name="links")


class UserProfile(models.Model):
    cba_username = models.CharField("CBA Username", blank=True, max_length=60)
    cba_user_token = models.CharField("CBA Token", blank=True, max_length=255)
    created_at = models.DateTimeField(auto_now_add=True)
    created_by = models.CharField(max_length=150)
    updated_at = models.DateTimeField(auto_now=True, blank=True, null=True)
    updated_by = models.CharField(blank=True, default="", max_length=150)
    user = models.OneToOneField(User, null=True, on_delete=models.SET_NULL, related_name="profile")

    def __str__(self):
        return self.user.username