# status.py
#
# Part of kepi.
# Copyright (c) 2018-2020 Marnanel Thurman.
# Licensed under the GNU Public License v2.

import logging
logger = logging.getLogger(name='kepi')

from django.db import models
from django.db.models.constraints import UniqueConstraint
from django.contrib.auth.models import AbstractUser
from django.conf import settings
import kepi.bowler_pub.crypto as crypto
from kepi.bowler_pub.utils import uri_to_url, is_local
import kepi.trilby_api.utils as trilby_utils
import kepi.trilby_api.signals as trilby_signals
from django.utils.timezone import now
from django.core.exceptions import ValidationError
from polymorphic.models import PolymorphicModel
import markdown

PUBLIC = "https://www.w3.org/ns/activitystreams#Public"

class Status(PolymorphicModel):

    class Meta:
        verbose_name_plural = 'Statuses'

    @classmethod
    def local_form(cls):
        return Status

    @classmethod
    def remote_form(cls):
        return Status

    # TODO: The original design has the serial number
    # monotonically but unpredictably increasing.

    remote_url = models.URLField(
            max_length = 255,
            null = True,
            blank = True,
            unique = True,
            )

    account = models.ForeignKey(
            'Person',
            related_name = 'poster',
            on_delete = models.DO_NOTHING,
            )

    in_reply_to = models.ForeignKey(
            'self',
            related_name = 'replies',
            on_delete = models.DO_NOTHING,
            null = True,
            blank = True,
            )

    content_source = models.TextField(
            help_text = 'Text of the status, as entered',
        )

    content_as_html_denormed = models.TextField(
            help_text = 'HTML rendering of content_source. Do not edit!',
            editable = False, 
            null = True,
            default = None,
        )

    created_at = models.DateTimeField(
            default = now,
            )

    # TODO Media

    sensitive = models.BooleanField(
            default = False,
            )

    spoiler_source = models.CharField(
            max_length = 255,
            null = True,
            blank = True,
            default = '',
            )

    spoiler_as_html_denormed = models.CharField(
            max_length = 255,
            null = True,
            editable = False,
            default = None,
            )

    visibility = models.CharField(
            max_length = 1,
            default = trilby_utils.VISIBILITY_PUBLIC,
            choices = trilby_utils.VISIBILITY_CHOICES,
            help_text = "Visiblity of this status.\n\n"+\
                    trilby_utils.VISIBILITY_HELP_TEXT,
            )

    language = models.CharField(
            max_length = 255,
            null = True,
            default = settings.KEPI['LANGUAGES'][0],
            )

    reblog_of = models.ForeignKey(
        'self',
        related_name = 'reblogs',
        on_delete = models.CASCADE,
        null = True,
        blank = True,
        )

    idempotency_key = models.CharField(
            max_length = 255,
            null = True,
            default = None,
            )

    @property
    def content_as_html(self):
        """
        Returns an HTML rendition of content_source.
        The return value will be cached.
        Saving the record will clear this cache.
        """

        if self.content_as_html_denormed is not None:
            return self.content_as_html_denormed

        if self.content_source is None:
            result = '<p></p>'
        else:
            result = markdown.markdown(self.content_source)

        self.content_as_html_denormed = result
        return result

    @property
    def spoiler_as_html(self):
        """
        Returns an HTML rendition of spoiler_source.
        The return value will be cached.
        Saving the record will clear this cache.
        """

        if self.spoiler_as_html_denormed is not None:
            return self.spoiler_as_html_denormed

        if self.spoiler_source is None:
            result = '<p></p>'
        else:
            result = markdown.markdown(self.spoiler_source)

        self.spoiler_as_html_denormed = result
        return result

    @property
    def emojis(self):
        return [] # TODO

    @property
    def reblogs_count(self):
        return self.reblogs.count()

    @property
    def favourites_count(self):
        return 0 # FIXME

    @property
    def original(self):
        result = self.reblog_of

        if result is None:
            return self

        if result.reblog_of is not None:
            # Reblog of reblog, which is invalid
            return self

        return result

    @property
    def reblogged(self):
        return self.reblogs.exists()

    @property
    def favourited(self):
        return False # FIXME

    @property
    def muted(self):
        return False # FIXME

    @property
    def pinned(self):
        return False # FIXME

    @property
    def media_attachments(self):
        return [] # FIXME

    @property
    def tags(self):
        import kepi.trilby_api.models.person as trilby_person

        return list(trilby_person.Person.objects.filter(
                mention__status = self,
                ))

    @property
    def card(self):
        return None # FIXME

    @property
    def poll(self):
        return None # FIXME

    @property
    def application(self):
        return None # FIXME

    @property
    def conversation(self):
        return 'conversation' # FIXME

    @property
    def in_reply_to_account_id(self):
        return self.in_reply_to.account.id

    @property
    def uri(self):
        # I know this property is called "uri", but
        # this matches the behaviour of Mastodon
        return self.url

    @property
    def url(self):
        if self.remote_url is not None:
            return self.remote_url

        return uri_to_url(settings.KEPI['STATUS_LINK'] % {
                'username': self.account.username,
                'id': self.id,
                })

    @property
    def activity_url(self):
        if self.remote_url is not None:
            raise ValueError(
                    "Activity URL is not stored for remote statuses",
                    )

        return uri_to_url(settings.KEPI['STATUS_ACTIVITY_LINK'] % {
                'username': self.account.username,
                'id': self.id,
                })

    @property
    def ancestors(self):

        result = []
        parent = self.in_reply_to

        while parent is not None:
            result.insert(0, parent)
            parent = parent.in_reply_to

        return result

    @property
    def descendants(self):

        result = []
        current = self

        while True:
            try:
                child = Status.objects.get(
                        in_reply_to = current,
                        )
            except Status.DoesNotExist:
                break

            result.append(child)
            current = child

        return result

    @property
    def thread(self):

        result = self.ancestors
        result.append(self)
        result.extend(self.descendants)

        return result

    @property
    def to(self):
        return [PUBLIC] # FIXME

    @property
    def cc(self):
        return [] # FIXME

    def save(self,
            send_signal = False,
            *args, **kwargs):

        newly_made = self.pk is None

        if self.reblog_of == self:
            raise ValueError("Status can't be a reblog of itself")

        if self.in_reply_to == self:
            raise ValueError("Status can't be a reply to itself")

        if not newly_made:
            old = self.__class__.objects.get(pk=self.pk)

            if self.content_source != old.content_source:
                logger.debug("%s: content changed; flushing HTML cache",
                        self)
                self.content_as_html_denormed = None

            if self.spoiler_source != old.spoiler_source:
                logger.debug("%s: spoiler changed; flushing HTML cache",
                        self)
                self.spoiler_as_html_denormed = None

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

        if send_signal and newly_made:

            if self.reblog_of is None:
                trilby_signals.posted.send(sender=self)
            else:
                trilby_signals.reblogged.send(sender=self)

    def __str__(self):
        return '%s: %s' % (
                self.id,
                self.content_source,
                )

    @classmethod
    def lookup(cls, url):

        # TODO: not yet tested
        # FIXME: if url is local, parse and return local <-- current breakage XXX
        # FIXME: if remote is not found, *possibly* create and return?

        if is_local(url):

            view = trilby_utils.find_local_view(
                    url,
                    )

            view = trilby_utils.find_local_view(
                    url,
                    which_views = ['StatusView'],
                    )

            if view is None:
                return None

            statusid = int(view.kwargs['status'])

            try:
                result = cls.objects.get(
                        id=statusid,
                        )
            except cls.DoesNotExist:
                logger.debug('%s is local but does not exist',
                        url)
                return None

            if result.account.local_user.username != view.kwargs['username']:
                logger.debug('%s is local but the username doesn\'t match',
                        url)
                return None

            logger.debug('%s is local and exists: %s',
                    url, result)
            return result

        # so, it's remote

        try:
            result = cls.objects.get(remote_url = url)
            logger.debug('%s is remote and exists: %s',
                    url, result)

            return result
        except cls.DoesNotExist:
            pass

        logger.debug('%s is unknown',
                url)

        return None

    @property
    def is_reply(self):
        return self.in_reply_to is not None

    @property
    def text(self):
        # XXX It's possible that one of (text, content) is
        # HTML and one is plain text. But the docs don't
        # seem to be forthcoming on this point, so we'll
        # just have to wait until we find out.
        return self.content_source

    @property
    def is_local(self):
        return self.remote_url is None
