#
# 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/>.
#
import dataclasses
import datetime

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

from django.contrib.auth.models import User
from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Max, Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _


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, help_text="True if type is active.")
    name = models.CharField(max_length=32, help_text="Name of the type.")
    slug = models.SlugField(max_length=32, unique=True, help_text="Slug of the type.")

    class Meta:
        ordering = ("name",)

    def __str__(self):
        return self.name


class Category(models.Model):
    description = models.TextField(blank=True, help_text="Description of the category.")
    is_active = models.BooleanField(default=True, help_text="True if category is active.")
    name = models.CharField(max_length=32, help_text="Name of the category.")
    slug = models.SlugField(max_length=32, unique=True, help_text="Slug of the category.")
    subtitle = models.CharField(blank=True, max_length=32, help_text="Subtitle of the category.")

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

    def __str__(self):
        return self.name


class Topic(models.Model):
    is_active = models.BooleanField(default=True, help_text="True if topic is active.")
    name = models.CharField(max_length=32, help_text="Name of the topic.")
    slug = models.SlugField(max_length=32, unique=True, help_text="Slug of the topic.")

    class Meta:
        ordering = ("name",)

    def __str__(self):
        return self.name


class MusicFocus(models.Model):
    is_active = models.BooleanField(default=True, help_text="True if music focus is active.")
    name = models.CharField(max_length=32, help_text="Name of the music focus.")
    slug = models.SlugField(max_length=32, unique=True, help_text="Slug of the music focus.")

    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, help_text="True if funding category is active.")
    name = models.CharField(max_length=32, help_text="Name of the funding category.")
    slug = models.SlugField(max_length=32, unique=True, help_text="Slug of the funding category.")

    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, help_text="True if language is active.")
    name = models.CharField(max_length=32, help_text="Name of the language.")

    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, help_text="True if license needs an author.")
    requires_express_permission_for_publication = models.BooleanField(
        default=True, help_text="True if express permission for publication required."
    )
    url = models.URLField(default="", blank=True, help_text="URL of the licence.")

    class Meta:
        ordering = ("name",)

    def __str__(self):
        return self.identifier


class Image(models.Model):
    alt_text = models.TextField(blank=True, default="", help_text="Alternate text for the image.")
    credits = models.TextField(blank=True, default="", help_text="Credits of the image")
    is_use_explicitly_granted_by_author = models.BooleanField(
        default=False, help_text="True if use is explicitly granted by author."
    )
    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",
        help_text="The URI of the image.",
    )
    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 render(self, width: int | None = None, height: int | None = None):
        if width is None and height is None:
            return self.image.url
        elif width and height:
            return self.image.crop[f"{width}x{height}"].url
        aspect_ratio = self.width / self.height
        if width is None:
            width = int(height * aspect_ratio)
        if height is None:
            height = int(width / aspect_ratio)
        return self.image.thumbnail[f"{width}x{height}"].url


class Profile(models.Model):
    biography = models.TextField(blank=True, help_text="Biography of the profile.")
    created_at = models.DateTimeField(auto_now_add=True)
    created_by = models.CharField(max_length=150)
    email = models.EmailField(blank=True, help_text="Email address of the profile.")
    image = models.ForeignKey(Image, null=True, on_delete=models.CASCADE, related_name="profiles")
    is_active = models.BooleanField(default=True, help_text="True if the profile is active.")
    name = models.CharField(max_length=128, help_text="Display name of the profile.")
    owners = models.ManyToManyField(
        User, blank=True, related_name="profiles", help_text="User ID(s) that own this profile."
    )
    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",)
        permissions = [
            ("edit__profile__biography", "Can edit biography field"),
            ("edit__profile__email", "Can edit email field"),
            ("edit__profile__image", "Can edit image field"),
            ("edit__profile__links", "Can edit links field"),
            ("edit__profile__name", "Can edit name field"),
            ("edit__profile__owners", "Can edit owners field"),
            # overrides ownership
            ("update_profile", "Can update profile"),
        ]

    def __str__(self):
        return self.name


class LinkType(models.Model):
    is_active = models.BooleanField(default=True, help_text="True if link type is active.")
    name = models.CharField(max_length=32, help_text="Name of the link type")

    class Meta:
        ordering = ("name",)

    def __str__(self):
        return self.name


class Link(models.Model):
    type = models.ForeignKey(LinkType, default=1, on_delete=models.CASCADE)
    url = models.URLField()

    class Meta:
        abstract = True

    def __str__(self):
        return self.url


class ProfileLink(Link):
    profile = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name="links")


class ShowManager(models.Manager):
    def with_max_timeslot_start(self):
        return (
            super().get_queryset().annotate(max_timeslot_start=Max("schedules__timeslots__start"))
        )


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, help_text="Description of this show.")
    email = models.EmailField(blank=True, null=True, help_text="Email address of this show.")
    funding_category = models.ForeignKey(
        FundingCategory, blank=True, null=True, on_delete=models.CASCADE, related_name="shows"
    )
    hosts = models.ManyToManyField(Profile, blank=True, related_name="shows")
    image = models.ForeignKey(Image, null=True, on_delete=models.CASCADE, related_name="shows")
    internal_note = models.TextField(blank=True, help_text="Internal note for this show.")
    is_active = models.BooleanField(default=True, help_text="True if this show is active.")
    is_public = models.BooleanField(default=False, help_text="True if this show is public.")
    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, help_text="Name of this Show.")
    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(help_text="Short description of this show.")
    slug = models.SlugField(
        blank=True, max_length=255, unique=True, help_text="Slug of this show."
    )
    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)

    objects = ShowManager()

    class Meta:
        ordering = ("slug",)
        permissions = [
            ("display__show__internal_note", "Can display internal note field"),
            ("edit__show__categories", "Can edit category field"),
            ("edit__show__cba_series_id", "Can edit cba series id field"),
            ("edit__show__default_playlist_id", "Can edit default media source"),
            ("edit__show__description", "Can edit description field"),
            ("edit__show__email", "Can edit email field"),
            ("edit__show__funding_categories", "Can edit funding category field"),
            ("edit__show__hosts", "Can edit hosts field"),
            ("edit__show__image", "Can edit image field"),
            ("edit__show__internal_note", "Can edit internal note field"),
            ("edit__show__is_active", "Can edit is active field"),
            ("edit__show__languages", "Can edit language field"),
            ("edit__show__links", "Can edit links field"),
            ("edit__show__logo", "Can edit logo field"),
            ("edit__show__music_focuses", "Can edit music focus field"),
            ("edit__show__name", "Can edit name field"),
            ("edit__show__owners", "Can edit owners field"),
            ("edit__show__predecessor", "Can edit predecessor field"),
            ("edit__show__short_description", "Can edit short description field"),
            ("edit__show__slug", "Can edit slug field"),
            ("edit__show__topics", "Can edit topic field"),
            ("edit__show__type", "Can edit type field"),
            # overrides ownership
            ("update_show", "Can update show"),
        ]

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        now = timezone.datetime.now()
        today = now.date()

        if self.pk and self.is_active is False:
            # deactivating a show means:
            # - **delete all* the timeslots that belong to a schedule of this show the after now
            # - **update all** the schedules of this show have today as `last_date`
            TimeSlot.objects.filter(schedule__show=self, start__gt=now).delete()
            self.schedules.filter(Q(last_date__gt=today) | Q(last_date=None)).update(
                last_date=today
            )
        super().save(*args, **kwargs)


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")
        permissions = [
            ("edit__schedule__default_playlist_id", "Can edit default media source"),
        ]

    def __str__(self):
        WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]
        start_time = self.start_time.strftime("%H:%M")
        end_time = self.end_time.strftime("%H:%M")
        recurrence = self.rrule.name
        weekday = self.first_date.weekday()

        return f"{self.show.name} - {recurrence} {WEEKDAYS[weekday]} {start_time}-{end_time}"


class TimeSlot(models.Model):
    end = models.DateTimeField()
    memo = models.TextField(blank=True, help_text="Memo for this timeslot.")
    playlist_id = models.IntegerField(null=True, help_text="Playlist ID of this timeslot.")
    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()

    class Meta:
        ordering = ("start", "end")
        permissions = [
            ("edit__timeslot__memo", "Can edit memo field"),
            ("edit__timeslot__playlist_id", "Can edit media source"),
            ("edit__timeslot__repetition_of", "Can edit repetition of field"),
        ]

    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, help_text="CBA entry ID.")
    content = models.TextField(help_text="Textual content of the note.")
    contributors = models.ManyToManyField(
        Profile, related_name="notes", help_text="Profile IDs that contributed to this episode."
    )
    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")
    language = models.ManyToManyField(Language, blank=True, related_name="episodes")
    playlist = models.TextField(blank=True)
    summary = models.TextField(blank=True, help_text="Summary of the Note.")
    tags = models.JSONField(blank=True, default=list)
    timeslot = models.OneToOneField(TimeSlot, null=True, on_delete=models.SET_NULL, unique=True)
    title = models.CharField(
        blank=True, default="", max_length=128, help_text="Title of the note."
    )
    topic = models.ManyToManyField(Topic, blank=True, related_name="episodes")
    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",)
        permissions = [
            ("edit__note__cba_id", "Can edit CBA id field"),
            ("edit__note__content", "Can edit content field"),
            ("edit__note__contributors", "Can edit contributor field"),
            ("edit__note__image", "Can edit image field"),
            ("edit__note__languages", "Can edit language field"),
            ("edit__note__links", "Can edit links field"),
            ("edit__note__playlist", "Can edit playlist field"),
            ("edit__note__summary", "Can edit summary field"),
            ("edit__note__tags", "Can edit tags field"),
            ("edit__note__title", "Can edit title field"),
            ("edit__note__topics", "Can edit topics field"),
            # overrides ownership
            ("create_note", "Can create note"),
            ("update_note", "Can update note"),
        ]

    def __str__(self):
        return self.title


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


class CBA(models.Model):
    username = models.CharField("Username", blank=True, max_length=60)
    user_token = models.CharField("User 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="cba")

    class Meta:
        permissions = [
            # overrides ownership
            ("create_cba", "Can create user CBA profile"),
            ("update_cba", "Can update user CBA profile"),
        ]

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


class Playlist(models.Model):
    class Meta:
        permissions = [
            ("add__file", "Can add file media-source"),
            ("add__import", "Can add import media-source"),
            ("add__line", "Can add line media-source"),
            ("add__m3ufile", "Can add m3u media-source"),
            ("add__stream", "Can add stream media-source"),
        ]


class ImageAspectRadioField(models.CharField):
    validators = [
        RegexValidator(
            code="invalid_aspect_ratio",
            message="Enter a valid aspect ratio in the format int:int or float:float",
            regex=r"^\d+(\.\d+)?:\d+(\.\d+)?$",
        )
    ]

    def __init__(self, *args, **kwargs):
        kwargs["max_length"] = 11

        super().__init__(*args, **kwargs)


class ImageShapeField(models.CharField):
    def __init__(self, *args, **kwargs):
        kwargs["choices"] = [
            ("rect", "rect"),
            ("round", "round"),
        ]
        kwargs["max_length"] = 5

        super().__init__(*args, **kwargs)


def validate_json_value(value: list | dict, schema: dict) -> None:
    """Validates value JSON against the schema. Raises a Django `ValidationError` if invalid."""

    try:
        jsonschema.validate(instance=value, schema=schema)
    except jsonschema.exceptions.ValidationError as e:
        raise DjangoValidationError(e.args[0])


def validate_cba_domains(value):
    schema = {
        "type": "array",
        "items": {"type": "string"},
    }

    validate_json_value(value, schema)


def validate_line_in_channels(value):
    schema = {
        "type": "object",
        "patternProperties": {
            "^.*$": {"type": "string"},
        },
    }

    validate_json_value(value, schema)


def validate_fallback_pools(value):
    schema = {
        "type": "object",
        "patternProperties": {
            "^.*$": {"type": "string"},
        },
    }

    validate_json_value(value, schema)


def validate_fallback_default_pool(value):
    if value not in RadioSettings.objects.first().pools.keys():
        raise DjangoValidationError(f"Pool key '{value}' does not exist in pools.")


class RadioSettings(models.Model):
    cba_api_key = models.CharField(blank=True, max_length=64, verbose_name="CBA API key")
    cba_domains = models.JSONField(
        blank=True,
        default=list,
        help_text="JSON array of strings",
        validators=[validate_cba_domains],
        verbose_name="CBA domains",
    )
    fallback_default_pool = models.CharField(
        blank=True, max_length=32, validators=[validate_fallback_default_pool]
    )
    fallback_show = models.ForeignKey(
        Show, blank=True, null=True, on_delete=models.CASCADE, related_name="+"
    )
    line_in_channels = models.JSONField(
        blank=True,
        default=dict,
        help_text="JSON key/value pairs",
        validators=[validate_line_in_channels],
    )
    micro_show = models.ForeignKey(
        Show, blank=True, null=True, on_delete=models.CASCADE, related_name="+"
    )
    note_image_aspect_ratio = ImageAspectRadioField(default="16:9")
    note_image_shape = ImageShapeField(default="rect")
    pools = models.JSONField(
        blank=True,
        default=dict,
        help_text="JSON key/value pairs",
        validators=[validate_fallback_pools],
    )
    profile_image_aspect_ratio = ImageAspectRadioField(default="1:1")
    profile_image_shape = ImageShapeField(default="round")
    show_image_aspect_ratio = ImageAspectRadioField(default="16:9")
    show_image_shape = ImageShapeField(default="rect")
    show_logo_aspect_ratio = ImageAspectRadioField(default="1:1")
    show_logo_shape = ImageShapeField(default="rect")
    station_logo = VersatileImageField(
        blank=True,
        height_field="station_logo_height",
        null=True,
        upload_to="images",
        width_field="station_logo_width",
    )
    station_logo_height = models.PositiveIntegerField(blank=True, null=True)
    station_logo_width = models.PositiveIntegerField(blank=True, null=True)
    station_name = models.CharField(max_length=256, unique=True)
    station_website = models.URLField()

    class Meta:
        verbose_name_plural = "Radio Settings"

    def __str__(self):
        return self.station_name


@dataclasses.dataclass()
class ProgramEntry:
    id: str
    start: datetime.datetime
    end: datetime.datetime
    show: Show
    timeslot: TimeSlot | None

    def playlist_id(self) -> int | None:
        if self.timeslot and self.timeslot.playlist_id:
            return self.timeslot.playlist_id
        else:
            return self.show.default_playlist_id