#
# Copyright 2016,2017,2018
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a 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 General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
from __future__ import annotations

from datetime import datetime
from datetime import timezone as tz
from itertools import chain
from typing import Self

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _

from club.models import Club
from core.models import Group, User


# Those functions prevent generating migration upon settings changes
def get_default_edit_group():
    return [settings.SITH_GROUP_OLD_SUBSCRIBERS_ID]


def get_default_view_group():
    return [settings.SITH_GROUP_PUBLIC_ID]


class Forum(models.Model):
    """The Forum class, made as a tree to allow nice tidy organization.

    owner_club allows club members to moderate there own topics
    edit_groups allows to put any group as a forum admin
    view_groups allows some groups to view a forum
    """

    id = models.AutoField(primary_key=True, db_index=True)
    name = models.CharField(_("name"), max_length=64)
    description = models.CharField(_("description"), max_length=512, default="")
    is_category = models.BooleanField(_("is a category"), default=False)
    parent = models.ForeignKey(
        "Forum",
        related_name="children",
        null=True,
        blank=True,
        on_delete=models.CASCADE,
    )
    owner_club = models.ForeignKey(
        Club,
        related_name="owned_forums",
        verbose_name=_("owner club"),
        default=settings.SITH_MAIN_CLUB_ID,
        on_delete=models.CASCADE,
    )
    edit_groups = models.ManyToManyField(
        Group,
        related_name="editable_forums",
        blank=True,
        default=get_default_edit_group,
    )
    view_groups = models.ManyToManyField(
        Group,
        related_name="viewable_forums",
        blank=True,
        default=get_default_view_group,
    )
    number = models.IntegerField(
        _("number to choose a specific forum ordering"), default=1
    )
    _last_message = models.ForeignKey(
        "ForumMessage",
        related_name="forums_where_its_last",
        verbose_name=_("the last message"),
        null=True,
        on_delete=models.SET_NULL,
    )
    _topic_number = models.IntegerField(_("number of topics"), default=0)

    class Meta:
        ordering = ["number"]

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        copy_rights = False
        if self.id is None:
            copy_rights = True
        super().save(*args, **kwargs)
        if copy_rights:
            self.copy_rights()

    def get_absolute_url(self):
        return reverse("forum:view_forum", kwargs={"forum_id": self.id})

    def clean(self):
        self.check_loop()

    def set_topic_number(self):
        self._topic_number = self.get_topic_number()
        self.save()
        if self.parent:
            self.parent.set_topic_number()

    def set_last_message(self):
        topic = (
            ForumTopic.objects.filter(forum__id=self.id)
            .exclude(_last_message=None)
            .order_by("-_last_message__id")
            .first()
        )
        forum = (
            Forum.objects.filter(parent__id=self.id)
            .exclude(_last_message=None)
            .order_by("-_last_message__id")
            .first()
        )
        if topic and forum:
            if topic._last_message_id < forum._last_message_id:
                self._last_message_id = forum._last_message_id
            else:
                self._last_message_id = topic._last_message_id
        elif topic:
            self._last_message_id = topic._last_message_id
        elif forum:
            self._last_message_id = forum._last_message_id
        self.save()
        if self.parent:
            self.parent.set_last_message()

    def apply_rights_recursively(self):
        children = self.children.all()
        for c in children:
            c.copy_rights()
            c.apply_rights_recursively()

    def copy_rights(self):
        """Copy, if possible, the rights of the parent folder."""
        if self.parent is not None:
            self.owner_club = self.parent.owner_club
            self.edit_groups.set(self.parent.edit_groups.all())
            self.view_groups.set(self.parent.view_groups.all())
            self.save()

    _club_memberships = {}  # This cache is particularly efficient:

    # divided by 3 the number of requests on the main forum page
    # after the first load
    def is_owned_by(self, user):
        if user.is_anonymous:
            return False
        if user.is_root or user.is_in_group(pk=settings.SITH_GROUP_FORUM_ADMIN_ID):
            return True
        try:
            m = Forum._club_memberships[self.id][user.id]
        except KeyError:
            m = self.owner_club.get_membership_for(user)
            try:
                Forum._club_memberships[self.id][user.id] = m
            except KeyError:
                Forum._club_memberships[self.id] = {}
                Forum._club_memberships[self.id][user.id] = m
        if m:
            return m.role > settings.SITH_MAXIMUM_FREE_ROLE
        return False

    def check_loop(self):
        """Raise a validation error when a loop is found within the parent list."""
        objs = []
        cur = self
        while cur.parent is not None:
            if cur in objs:
                raise ValidationError(_("You can not make loops in forums"))
            objs.append(cur)
            cur = cur.parent

    def get_full_name(self):
        return "/".join(
            chain.from_iterable(
                [[parent.name for parent in self.get_parent_list()], [self.name]]
            )
        )

    @cached_property
    def parent_list(self):
        return self.get_parent_list()

    def get_parent_list(self):
        parents = []
        current = self.parent
        while current is not None:
            parents.append(current)
            current = current.parent
        return parents

    @property
    def topic_number(self):
        return self._topic_number

    def get_topic_number(self):
        number = self.topics.all().count()
        for c in self.children.all():
            number += c.topic_number
        return number

    @cached_property
    def last_message(self):
        return self._last_message

    def get_children_list(self) -> list[Self]:
        children = [self.id]
        for c in self.children.all():
            children.append(c.id)
            children.extend(c.get_children_list())
        return children


class ForumTopic(models.Model):
    forum = models.ForeignKey(Forum, related_name="topics", on_delete=models.CASCADE)
    author = models.ForeignKey(
        User, related_name="forum_topics", on_delete=models.CASCADE
    )
    description = models.CharField(_("description"), max_length=256, default="")
    subscribed_users = models.ManyToManyField(
        User, related_name="favorite_topics", verbose_name=_("subscribed users")
    )
    _last_message = models.ForeignKey(
        "ForumMessage",
        related_name="+",
        verbose_name=_("the last message"),
        null=True,
        on_delete=models.SET_NULL,
    )
    _title = models.CharField(_("title"), max_length=64, blank=True)
    _message_number = models.IntegerField(_("number of messages"), default=0)

    class Meta:
        ordering = ["-_last_message__date"]

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        self.forum.set_topic_number()  # Recompute the cached value
        self.forum.set_last_message()

    def get_absolute_url(self):
        return reverse("forum:view_topic", kwargs={"topic_id": self.id})

    def is_owned_by(self, user):
        return self.forum.is_owned_by(user)

    def can_be_edited_by(self, user):
        return user.is_root or user.can_edit(self.forum)

    def can_be_viewed_by(self, user):
        return user.is_root or user.can_view(self.forum)

    def get_first_unread_message(self, user: User) -> ForumMessage | None:
        if not hasattr(user, "forum_infos"):
            return None
        return (
            self.messages.exclude(readers=user)
            .filter(date__gte=user.forum_infos.last_read_date)
            .order_by("id")
            .first()
        )

    @cached_property
    def last_message(self):
        return self._last_message

    @cached_property
    def title(self):
        return self._title


class ForumMessage(models.Model):
    """A message in the forum (thx Cpt. Obvious.)."""

    topic = models.ForeignKey(
        ForumTopic, related_name="messages", on_delete=models.CASCADE
    )
    author = models.ForeignKey(
        User, related_name="forum_messages", on_delete=models.CASCADE
    )
    title = models.CharField(_("title"), default="", max_length=64, blank=True)
    message = models.TextField(_("message"), default="")
    date = models.DateTimeField(_("date"), default=timezone.now)
    readers = models.ManyToManyField(
        User, related_name="read_messages", verbose_name=_("readers")
    )
    _deleted = models.BooleanField(_("is deleted"), default=False)

    class Meta:
        ordering = ["-date"]

    def __str__(self):
        return "%s (%s) - %s" % (self.id, self.author, self.title)

    def save(self, *args, **kwargs):
        self._deleted = self.is_deleted()  # Recompute the cached value
        super().save(*args, **kwargs)
        if self.is_last_in_topic():
            self.topic._last_message_id = self.id
        if self.is_first_in_topic() and self.title:
            self.topic._title = self.title
        self.topic._message_number = self.topic.messages.count()
        self.topic.save()

    def get_absolute_url(self):
        return reverse("forum:view_message", kwargs={"message_id": self.id})

    def is_first_in_topic(self):
        return bool(self.id == self.topic.messages.order_by("date").first().id)

    def is_last_in_topic(self):
        return bool(self.id == self.topic.messages.order_by("date").last().id)

    def is_owned_by(self, user):
        if user.is_anonymous:
            return False
        # Anyone can create a topic: it's better to
        # check the rights at the forum level, since it's more controlled
        return self.topic.forum.is_owned_by(user) or user.id == self.author.id

    def can_be_edited_by(self, user):
        return user.can_edit(self.topic.forum)

    def can_be_viewed_by(self, user):
        # No need to check the real rights since it's already done by the Topic view
        # and it impacts performances too much
        return not self._deleted

    def can_be_moderated_by(self, user):
        return self.topic.forum.is_owned_by(user) or user.id == self.author_id

    def get_url(self):
        return (
            self.topic.get_absolute_url()
            + "?page="
            + str(self.get_page())
            + "#msg_"
            + str(self.id)
        )

    def get_page(self):
        return (
            int(
                self.topic.messages.filter(id__lt=self.id).count()
                / settings.SITH_FORUM_PAGE_LENGTH
            )
            + 1
        )

    def mark_as_read(self, user):
        if user.is_anonymous:
            return
        if not self.is_read(user):
            self.readers.add(user)

    def is_read(self, user):
        return (self.date < user.forum_infos.last_read_date) or (
            user in self.readers.all()
        )

    def is_deleted(self):
        if self._state.adding:
            return False
        meta = self.metas.exclude(action="EDIT").order_by("-date").first()
        if meta:
            return meta.action == "DELETE"
        return False


MESSAGE_META_ACTIONS = [
    ("EDIT", _("Message edited by")),
    ("DELETE", _("Message deleted by")),
    ("UNDELETE", _("Message undeleted by")),
]


class ForumMessageMeta(models.Model):
    user = models.ForeignKey(
        User, related_name="forum_message_metas", on_delete=models.CASCADE
    )
    message = models.ForeignKey(
        ForumMessage, related_name="metas", on_delete=models.CASCADE
    )
    date = models.DateTimeField(_("date"), default=timezone.now)
    action = models.CharField(_("action"), choices=MESSAGE_META_ACTIONS, max_length=16)

    def __str__(self):
        return f"{self.user.nick_name} ({self.date})"

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        self.message._deleted = self.message.is_deleted()
        self.message.save()


class ForumUserInfo(models.Model):
    """The forum infos of a user.

    This currently stores only the last date a user clicked "Mark all as read".
    However, this can be extended with lot of user preferences dedicated to a
    user, such as the favourite topics, the signature, and so on...
    """

    user = models.OneToOneField(
        User, related_name="_forum_infos", on_delete=models.CASCADE
    )
    last_read_date = models.DateTimeField(
        _("last read date"),
        default=datetime(
            year=settings.SITH_SCHOOL_START_YEAR, month=1, day=1, tzinfo=tz.utc
        ),
    )

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