#       Licensed to the Apache Software Foundation (ASF) under one
#       or more contributor license agreements.  See the NOTICE file
#       distributed with this work for additional information
#       regarding copyright ownership.  The ASF licenses this file
#       to you under the Apache License, Version 2.0 (the
#       "License"); you may not use this file except in compliance
#       with the License.  You may obtain a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#       Unless required by applicable law or agreed to in writing,
#       software distributed under the License is distributed on an
#       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#       KIND, either express or implied.  See the License for the
#       specific language governing permissions and limitations
#       under the License.
from __future__ import annotations
import os
import logging
from cgi import FieldStorage
from datetime import datetime
import typing

import markupsafe
import pymongo
from pymongo.errors import DuplicateKeyError
from tg import tmpl_context as c, app_globals as g

from ming import schema
from ming.odm.base import session
from ming.odm.property import (FieldProperty, RelationProperty,
                               ForeignIdProperty)
from ming.utils import LazyProperty
from bson import ObjectId
from webob import exc

from allura.lib import helpers as h
from allura.lib import security
from allura.lib.security import require_access, has_access, is_denied
from allura.lib import utils
from allura.model.notification import Notification, Mailbox
from .artifact import Artifact, ArtifactReference, VersionedArtifact, Snapshot, Message, Feed, ReactableArtifact
from .attachments import BaseAttachment
from .auth import User, ProjectRole, AlluraUserProperty
from .timeline import ActivityObject
from .types import MarkdownCache

if typing.TYPE_CHECKING:
    from ming.odm.mapper import Query

log = logging.getLogger(__name__)


class Discussion(Artifact, ActivityObject):

    class __mongometa__:
        name = 'discussion'

    query: Query[Discussion]

    type_s = 'Discussion'

    parent_id = FieldProperty(schema.Deprecated)
    shortname = FieldProperty(str)
    name = FieldProperty(str)
    description = FieldProperty(str, if_missing='')
    description_cache = FieldProperty(MarkdownCache)
    num_topics = FieldProperty(int, if_missing=0)
    num_posts = FieldProperty(int, if_missing=0)

    threads = RelationProperty('Thread', via='discussion_id')
    posts = RelationProperty('Post', via='discussion_id')

    def __json__(self, limit=None, posts_limit=None, is_export=False):
        return dict(
            _id=str(self._id),
            shortname=self.shortname,
            name=self.name,
            description=self.description,
            threads=[t.__json__(limit=posts_limit, is_export=is_export) for t
                     in self.thread_class().query.find(dict(discussion_id=self._id)).limit(limit or 0)]
        )

    @property
    def activity_name(self):
        return 'discussion %s' % self.name

    @classmethod
    def thread_class(cls) -> Thread:
        return cls.threads.related

    @classmethod
    def post_class(cls) -> Post:
        return cls.posts.related

    @classmethod
    def attachment_class(cls):
        return DiscussionAttachment

    def update_stats(self):
        self.num_topics = self.thread_class().query.find(
            dict(discussion_id=self._id)).count()
        self.num_posts = self.post_class().query.find(
            dict(discussion_id=self._id, status='ok', deleted=False)).count()

    @LazyProperty
    def last_post(self):
        q = self.post_class().query.find(dict(
            discussion_id=self._id,
            status='ok',
            deleted=False,
        )).sort('timestamp', pymongo.DESCENDING).limit(1)
        return q.first()

    def url(self):
        return self.app.url + '_discuss/'

    def shorthand_id(self):
        return self.shortname

    def index(self):
        result = Artifact.index(self)
        result.update(
            title=self.name,
            name_s=self.name,
            text=self.description)
        return result

    def attach(self, filename, fp, **kw):
        """Attach a file to this Artifact.

        :param filename: file name
        :param fp: a file-like object (implements ``read()``)
        :param kw: passed through to Attachment class constructor

        Override's Artifact.attach to use a str artifact_id
        """
        att = self.attachment_class().save_attachment(
            filename=filename,
            fp=fp, artifact_id=str(self._id), **kw)
        return att

    def delete(self):
        # Delete all the threads
        for thread in self.threads:
            thread.delete()
        # threads handle deleting posts
        # super() handles artifacts, attachments, etc.
        super().delete()

    def find_posts(self, **kw):
        q = dict(kw, discussion_id=self._id, deleted=False)
        return self.post_class().query.find(q)


class Thread(Artifact, ActivityObject):

    class __mongometa__:
        name = 'thread'
        indexes = [
            ('artifact_id',),
            ('ref_id',),
            (('app_config_id', pymongo.ASCENDING),
             ('last_post_date', pymongo.DESCENDING),
             ('mod_date', pymongo.DESCENDING)),
            ('discussion_id', 'num_replies'),
        ]

    query: Query[Thread]

    type_s = 'Thread'

    _id = FieldProperty(str, if_missing=lambda: h.nonce(10))
    discussion_id = ForeignIdProperty(Discussion)
    ref_id: str = ForeignIdProperty('ArtifactReference')
    subject = FieldProperty(str, if_missing='')
    num_replies = FieldProperty(int, if_missing=0)
    num_views = FieldProperty(int, if_missing=0)
    first_post_id = ForeignIdProperty('Post')
    last_post_date = FieldProperty(datetime, if_missing=datetime(1970, 1, 1))
    artifact_reference = FieldProperty(schema.Deprecated)
    artifact_id = FieldProperty(schema.Deprecated)

    discussion = RelationProperty(Discussion)
    posts = RelationProperty('Post', via='thread_id')
    first_post = RelationProperty('Post', via='first_post_id')
    ref = RelationProperty('ArtifactReference')

    def should_update_index(self, old_doc, new_doc):
        """Skip index update if only `num_views` has changed.

        Value of `num_views` is updated whenever user loads thread page.
        This generates a lot of unnecessary `add_artifacts` tasks.
        """
        old_doc.pop('num_views', None)
        new_doc.pop('num_views', None)
        return old_doc != new_doc

    def attachment_for_export(self, page):
        return [dict(bytes=attach.length,
                     url=h.absurl(attach.url()),
                     path=os.path.join(
                         self.artifact.app_config.options.mount_point,
                         str(self.artifact._id),
                         self._id,
                         page.slug,
                         h.safe_filename(attach.filename))
                     ) for attach in page.attachments]

    def attachments_for_json(self, page):
        return [dict(bytes=attach.length,
                     url=h.absurl(attach.url())) for attach in page.attachments]

    def __json__(self, limit=None, page=None, is_export=False):
        return dict(
            _id=self._id,
            discussion_id=str(self.discussion_id),
            subject=self.subject,
            limit=limit,
            page=page,
            posts=[dict(slug=p.slug,
                        text=p.text,
                        subject=p.subject,
                        author=p.author().username,
                        author_icon_url=h.absurl(p.author().icon_url()),
                        timestamp=p.timestamp,
                        last_edited=p.last_edit_date,
                        attachments=self.attachment_for_export(p) if is_export else self.attachments_for_json(p),
                        is_meta=p.is_meta,
                        )
                   for p in self.query_posts(status='ok', style='chronological', limit=limit, page=page)
                   ]
        )

    @property
    def activity_name(self):
        return 'thread %s' % self.subject

    def parent_security_context(self):
        return self.discussion

    @classmethod
    def new(cls, **props):
        '''Creates a new Thread instance, ensuring a unique _id.'''
        for i in range(5):
            try:
                thread = cls(**props)
                session(thread).flush(thread)
                return thread
            except DuplicateKeyError as err:
                log.warning(
                    'Got DuplicateKeyError: attempt #%s, trying again. %s', i, err)
                if i == 4:
                    raise
                session(thread).expunge(thread)
                continue

    @classmethod
    def discussion_class(cls) -> Discussion:
        return cls.discussion.related

    @classmethod
    def post_class(cls) -> Post:
        return cls.posts.related

    @classmethod
    def attachment_class(cls):
        return DiscussionAttachment

    @property
    def artifact(self):
        # Threads attached to a wiki page, ticket, etc will have a .ref.artifact pointing to that WikiPage etc
        # Threads that are part of a forum will not have that
        if self.ref is None:
            return self.discussion
        return self.ref.artifact

    # Use wisely - there's .num_replies also
    @property
    def post_count(self):
        return Post.query.find(dict(
            discussion_id=self.discussion_id,
            thread_id=self._id,
            status={'$in': ['ok', 'pending']},
            deleted=False,
        )).count()

    def primary(self):
        if self.ref is None:
            return self
        return self.ref.artifact

    def post_to_feed(self, post):
        if post.status == 'ok':
            Feed.post(
                self.primary(),
                title=post.subject,
                description=post.text,
                link=post.url_paginated(),
                pubdate=post.mod_date,
            )

    def add_post(self, **kw):
        """Helper function to avoid code duplication."""
        p = self.post(**kw)
        p.commit(update_stats=False)
        session(self).flush(self)
        self.update_stats()
        if not self.first_post:
            self.first_post_id = p._id
        self.post_to_feed(p)
        return p

    def include_subject_in_spam_check(self, post):
        return (post.primary() == post  # only artifacts where the discussion is the main thing i.e. ForumPost
                and
                self.num_replies == 0)  # only first post in thread

    def is_spam(self, post):
        roles = [r.name for r in c.project.named_roles]
        spam_check_text = post.text
        if self.include_subject_in_spam_check(post):
            spam_check_text = self.subject + '\n' + spam_check_text
        spammy = g.spam_checker.check(spam_check_text, artifact=post, user=c.user)
        if c.user in c.project.users_with_role(*roles):
            # always run the check, so it's logged.  But don't act on it for admins/developers of their own project
            return False
        else:
            return spammy

    def post(self, text, message_id=None, parent_id=None, notify=True,
             notification_text=None, timestamp=None, ignore_security=False,
             is_meta=False, subscribe=False, **kw):
        if not ignore_security:
            require_access(self, 'post')
            # check app-level for Blocked Users, in addition to the standard `self` check above
            if is_denied(self.app, 'post', c.user, self.project):
                raise exc.HTTPForbidden
        if subscribe:
            self.primary().subscribe()
        if message_id is None:
            message_id = h.gen_message_id()
        parent = parent_id and self.post_class().query.get(_id=parent_id)
        slug, full_slug = self.post_class().make_slugs(parent, timestamp)
        kwargs = dict(
            discussion_id=self.discussion_id,
            full_slug=full_slug,
            slug=slug,
            thread_id=self._id,
            parent_id=parent_id,
            text=text,
            status='pending',
            is_meta=is_meta)
        if timestamp is not None:
            kwargs['timestamp'] = timestamp
        if message_id is not None:
            kwargs['_id'] = message_id
        post = self.post_class()(**kwargs)

        if ignore_security or is_meta:
            spammy = False
        else:
            spammy = self.is_spam(post)
        # unmoderated post -> autoapprove
        # unmoderated post but is spammy -> don't approve it, it goes into moderation
        # moderated post -> moderation
        # moderated post but is spammy -> mark as spam
        if ignore_security or (not spammy and has_access(self, 'unmoderated_post')):
            log.info('Auto-approving message from %s', c.user.username)
            file_info = kw.get('file_info', None)
            post.approve(file_info, notify=notify,
                         notification_text=notification_text)
        elif not has_access(self, 'unmoderated_post') and spammy:
            post.spam(submit_spam_feedback=False)  # no feedback since we're marking as spam automatically not manually
        else:
            self.notify_moderators(post)
        return post

    def notify_moderators(self, post):
        ''' Notify moderators that a post needs approval [#2963] '''
        artifact = self.artifact or self
        subject = '[{}:{}] Moderation action required'.format(
            c.project.shortname, c.app.config.options.mount_point)
        author = post.author()
        url = self.discussion_class().query.get(_id=self.discussion_id).url()
        text = ('The following submission requires approval at %s before '
                'it can be approved for posting:\n\n%s'
                % (h.absurl(url + 'moderate'), post.text))
        n = Notification(
            ref_id=artifact.index_id(),
            topic='message',
            link=artifact.url(),
            _id=artifact.url() + post._id,
            from_address=str(author._id) if author != User.anonymous()
            else None,
            reply_to_address=g.noreply,
            subject=subject,
            text=text,
            in_reply_to=post.parent_id,
            author_id=author._id,
            pubdate=datetime.utcnow())
        users = self.app_config.project.users()
        for u in users:
            if (has_access(self, 'moderate', u)
                and Mailbox.subscribed(user_id=u._id,
                                       app_config_id=post.app_config_id)):
                n.send_direct(str(u._id))

    def update_stats(self):
        self.num_replies = self.post_class().query.find(
            dict(thread_id=self._id, status='ok', deleted=False)).count()

    @LazyProperty
    def last_post(self):
        q = self.post_class().query.find(dict(
            thread_id=self._id,
            deleted=False,
        )).sort('timestamp', pymongo.DESCENDING)
        return q.first()

    def create_post_threads(self, posts):
        result = []
        post_index = {}
        for p in sorted(posts, key=lambda p: p.full_slug):
            pi = dict(post=p, children=[])
            post_index[p._id] = pi
            if p.parent_id in post_index:
                post_index[p.parent_id]['children'].append(pi)
            else:
                result.append(pi)
        return result

    def query_posts(self, page=None, limit=None,
                    timestamp=None, style='threaded', status=None):
        if timestamp:
            terms = dict(discussion_id=self.discussion_id, thread_id=self._id,
                         status={'$in': ['ok', 'pending']}, timestamp=timestamp)
        else:
            terms = dict(discussion_id=self.discussion_id, thread_id=self._id,
                         status={'$in': ['ok', 'pending']})
        if status:
            terms['status'] = status
        terms['deleted'] = False
        q = self.post_class().query.find(terms)
        if style == 'threaded':
            q = q.sort('full_slug')
        else:
            q = q.sort('timestamp')
        if limit is not None:
            limit = int(limit)
            if page is not None:
                q = q.skip(page * limit)
            q = q.limit(limit)
        return q

    def find_posts(self, *args, **kwargs):
        return self.query_posts(*args, **kwargs).all()

    def url(self):
        # Can't use self.discussion because it might change during the req
        discussion = self.discussion_class().query.get(_id=self.discussion_id)
        return discussion.url() + 'thread/' + str(self._id) + '/'

    def shorthand_id(self):
        return self._id

    def index(self):
        result = Artifact.index(self)
        result.update(
            title=self.subject or '(no subject)',
            name_s=self.subject,
            views_i=self.num_views,
            text=self.subject)
        return result

    def delete(self):
        for p in self.post_class().query.find(dict(thread_id=self._id)):
            p.delete()
        super().delete()

    def spam(self):
        """Mark this thread as spam."""
        for p in self.post_class().query.find(dict(thread_id=self._id)):
            p.spam()


class PostHistory(Snapshot):

    class __mongometa__:
        name = 'post_history'

    query: Query[PostHistory]

    artifact_id = ForeignIdProperty('Post')

    @classmethod
    def post_class(cls) -> Post:
        return cls.artifact_id.related

    def original(self):
        return self.post_class().query.get(_id=self.artifact_id)

    def shorthand_id(self):
        original = self.original()
        if original:
            return f'{original.shorthand_id()}#{self.version}'
        else:
            return None

    def url(self):
        if self.original():
            return self.original().url() + '?version=%d' % self.version
        else:
            return None

    def index(self):
        result = Snapshot.index(self)
        result.update(
            type_s='Post Snapshot',
            text=self.data.text)
        return result


class Post(Message, VersionedArtifact, ActivityObject, ReactableArtifact):

    class __mongometa__:
        name = 'post'
        history_class = PostHistory
        indexes = [
            # used in general lookups, last_post, etc
            ('discussion_id', 'status', 'timestamp'),
            # for update_stats()
            ('discussion_id', 'deleted', 'status'),
            # for update_stats() and thread_id in general
            ('thread_id', 'status', 'deleted'),
            # for post_count and find_posts/query_posts, including full_slug sort which is useful on super big threads
            ('deleted', 'discussion_id', 'thread_id', 'status', 'full_slug'),
        ]

    query: Query[Post]

    type_s = 'Post'

    thread_id = ForeignIdProperty(Thread)
    discussion_id = ForeignIdProperty(Discussion)
    subject = FieldProperty(schema.Deprecated)
    status = FieldProperty(schema.OneOf('ok', 'pending', 'spam',
                                        if_missing='pending'))
    last_edit_date = FieldProperty(datetime, if_missing=None)
    last_edit_by_id: ObjectId = AlluraUserProperty()
    edit_count = FieldProperty(int, if_missing=0)
    spam_check_id = FieldProperty(str, if_missing='')
    text_cache = FieldProperty(MarkdownCache)
    # meta comment - system generated, describes changes to an artifact
    is_meta = FieldProperty(bool, if_missing=False)

    thread = RelationProperty(Thread)
    discussion = RelationProperty(Discussion)

    def __json__(self):
        author = self.author()
        return dict(
            _id=str(self._id),
            thread_id=self.thread_id,
            slug=self.slug,
            subject=self.subject,
            status=self.status,
            text=self.text,
            timestamp=self.timestamp,
            last_edited=self.last_edit_date,
            author_id=str(author._id),
            author=author.username)

    @property
    def activity_name(self):
        return 'a comment'

    @property
    def activity_url(self):
        return self.url_paginated()

    def has_activity_access(self, perm, user, activity):
        """Return True if user has perm access to this object, otherwise
        return False.

        For the purposes of activitystreams, we're saying that the user does
        not have access to a 'comment' activity unless he also has access to
        the artifact on which it was posted (if there is one).
        """
        if self.project is None or self.deleted or self.status != 'ok':
            return False
        artifact_access = True
        if self.thread.artifact:
            if self.thread.artifact.project is None:
                return False
            if self.thread.artifact.deleted:
                return False
            artifact_access = security.has_access(self.thread.artifact, perm,
                                                  user, self.thread.artifact.project)

        return artifact_access and security.has_access(self, perm, user,
                                                       self.project)

    @property
    def activity_extras(self):
        d = ActivityObject.activity_extras.fget(self)
        # For activity summary, convert Post text to html,
        # strip all tags, and truncate
        LEN = 500
        summary = markupsafe.Markup.escape(
            g.markdown.cached_convert(self, 'text')).striptags()
        if len(summary) > LEN:
            split = max(summary.find(' ', LEN), LEN)
            summary = summary[:split] + '...'
        d.update(summary=summary)
        return d

    def index(self):
        result = super().index()
        result.update(
            title='Post by {} on {}'.format(
                self.author().username, self.subject),
            name_s=self.subject,
            type_s='Post',
            text=self.text)
        return result

    @classmethod
    def discussion_class(cls) -> Discussion:
        return cls.discussion.related

    @classmethod
    def thread_class(cls) -> Thread:
        return cls.thread.related

    @classmethod
    def attachment_class(cls):
        return DiscussionAttachment

    @property
    def parent(self):
        if self.parent_id:
            return self.query.get(_id=self.parent_id)

    @property
    def subject(self):
        subject = None
        if self.thread:
            subject = self.thread.subject
            if not subject:
                artifact = self.thread.artifact
                if artifact:
                    subject = getattr(artifact, 'email_subject', '')
        return subject or '(no subject)'

    def add_multiple_attachments(self, file_info: FieldStorage | list[FieldStorage]):
        if isinstance(file_info, list):
            for fi in file_info:
                self.add_attachment(fi)
        else:
            self.add_attachment(file_info)

    def add_attachment(self, file_info: FieldStorage):  # webob provides attachments as FieldStorage via legacy-cgi
        if hasattr(file_info, 'file'):
            mime_type = file_info.type
            if not mime_type or '/' not in mime_type:
                mime_type = utils.guess_mime_type(file_info.filename)
            self.attach(
                file_info.filename, file_info.file, content_type=mime_type,
                post_id=self._id,
                thread_id=self.thread_id,
                discussion_id=self.discussion_id)

    def last_edit_by(self):
        return User.query.get(_id=self.last_edit_by_id) or User.anonymous()

    def primary(self):
        return self.thread.primary()

    def url(self):
        if self.thread:
            return self.thread.url() + h.urlquote(self.slug) + '/'
        else:  # pragma no cover
            return None

    def parent_artifact(self):
        """
        :return: the artifact (e.g Ticket, Wiki Page) that this Post belongs to.  May return None.
        """
        aref = ArtifactReference.query.get(_id=self.thread.ref_id)
        if aref and aref.artifact:
            return aref.artifact
        else:
            return None

    def main_url(self):
        """
        :return: the URL for the artifact (e.g Ticket, Wiki Page) that this Post belongs to,
                 else the default thread URL
        """
        parent_artifact = self.parent_artifact()
        if parent_artifact:
            url = parent_artifact.url()
        else:
            url = self.thread.url()
        return url

    def url_paginated(self):
        '''Return link to the thread with a #target that poins to this comment.

        Also handle pagination properly.
        '''
        if not self.thread:  # pragma no cover
            return None
        limit, p, s = g.handle_paging(None, 0)  # get paging limit
        if self.query.find(dict(thread_id=self.thread._id)).count() <= limit:
            # all posts in a single page
            page = 0
        else:
            posts = self.thread.find_posts()
            posts = self.thread.create_post_threads(posts)

            def find_i(posts):
                '''Find the index number of this post in the display order'''
                q = []

                def traverse(posts):
                    for p in posts:
                        if p['post']._id == self._id:
                            return True  # found
                        q.append(p)
                        if traverse(p['children']):
                            return True
                traverse(posts)
                return len(q)

            page = find_i(posts) // limit

        slug = h.urlquote(self.slug)
        url = self.main_url()
        if page == 0:
            return f'{url}?limit={limit}#{slug}'
        return f'{url}?limit={limit}&page={page}#{slug}'

    def shorthand_id(self):
        if self.thread:
            return f'{self.thread.shorthand_id()}#{self.slug}'
        else:  # pragma no cover
            return None

    def link_text(self):
        return self.subject

    def reply_subject(self):
        if self.subject and self.subject.lower().startswith('re:'):
            return self.subject
        else:
            return 'Re: ' + (self.subject or '(no subject)')

    def delete(self):
        thread = self.thread
        super().delete()
        thread.update_stats()

    def soft_delete(self):
        self.deleted = True
        session(self).flush(self)
        self.thread.update_stats()

    def approve(self, file_info=None, notify=True, notification_text=None):
        if self.status == 'ok':
            return
        self.status = 'ok'
        author = self.author()
        author_role = ProjectRole.by_user(
            author, project=self.project, upsert=True)
        if not author.is_anonymous():
            security.simple_grant(
                self.acl, author_role._id, 'moderate')
        self.commit()
        if (c.app.config.options.get('PostingPolicy') == 'ApproveOnceModerated'
                and author._id is not None):
            security.simple_grant(
                self.acl, author_role._id, 'unmoderated_post')
        if notify:
            self.notify(file_info=file_info, notification_text=notification_text)
        artifact = self.thread.artifact or self.thread
        session(self).flush()
        self.thread.last_post_date = max(
            self.thread.last_post_date,
            self.mod_date)
        self.thread.update_stats()
        if hasattr(artifact, 'update_stats'):
            artifact.update_stats()
        if self.text and not self.is_meta:
            g.director.create_activity(author, 'posted', self, target=artifact,
                                       related_nodes=[self.app_config.project],
                                       tags=['comment'])

    def notify(self, file_info=None, notification_text=None):
        if self.project.notifications_disabled:
            return  # notifications disabled for entire project
        artifact = self.thread.artifact or self.thread
        msg_id = artifact.url() + self._id
        notification_params = dict(
            post=self,
            text=notification_text,
            file_info=file_info)
        n = Notification.query.get(_id=msg_id)
        if n and 'Moderation action required' in n.subject:
            # Existing notification for this artifact is for moderators only,
            # this means artifact was not auto approved, and all the
            # subscribers did not receive notification. Now, moderator approved
            # artifact/post, so we should re-send actual notification
            msg_id = 'approved-' + msg_id
            n = Notification.query.get(_id=msg_id)
            if n:
                # 'approved' notification also exists, re-send
                n.fire_notification_task([artifact, self.thread], 'message')
            else:
                # 'approved' notification does not exist, create
                notification_params['message_id'] = msg_id
        if not n:
            # artifact is Forum (or artifact like WikiPage)
            n = Notification.post(artifact, 'message',
                                  additional_artifacts_to_match_subscriptions=self.thread,
                                  **notification_params)
        if not n:
            return
        if getattr(artifact, 'monitoring_email', None):
            if hasattr(artifact, 'notify_post'):
                if artifact.notify_post:
                    n.send_simple(artifact.monitoring_email)
            else:  # Send if no extra checks required
                n.send_simple(artifact.monitoring_email)

    def spam(self, submit_spam_feedback=True):
        self.status = 'spam'
        if submit_spam_feedback:
            g.spam_checker.submit_spam(self.text, artifact=self, user=self.author())
        session(self).flush(self)
        self.thread.update_stats()

    def undo(self, prev_status):
        if prev_status in ('ok', 'pending'):
            self.status = prev_status
            session(self).flush(self)
            self.thread.update_stats()


class DiscussionAttachment(BaseAttachment):
    DiscussionClass = Discussion
    ThreadClass = Thread
    PostClass = Post
    ArtifactClass = Post
    thumbnail_size = (100, 100)

    class __mongometa__:
        polymorphic_identity = 'DiscussionAttachment'
        indexes = ['filename', 'discussion_id', 'thread_id', 'post_id']

    query: Query[DiscussionAttachment]

    discussion_id = FieldProperty(schema.ObjectId)
    thread_id = FieldProperty(str)
    post_id = FieldProperty(str)
    artifact_id = FieldProperty(str)
    attachment_type = FieldProperty(str, if_missing='DiscussionAttachment')

    @property
    def discussion(self):
        return self.DiscussionClass.query.get(_id=self.discussion_id)

    @property
    def thread(self):
        return self.ThreadClass.query.get(_id=self.thread_id)

    @property
    def post(self):
        return self.PostClass.query.get(_id=self.post_id)

    @classmethod
    def metadata_for(cls, post):
        return dict(
            post_id=post._id,
            thread_id=post.thread_id,
            discussion_id=post.discussion_id,
            app_config_id=post.app_config_id)

    def url(self):
        if self.post_id:
            return (self.post.url() + 'attachment/' +
                    h.urlquote(self.filename))
        elif self.thread_id:
            return (self.thread.url() + 'attachment/' +
                    h.urlquote(self.filename))
        else:
            return (self.discussion.url() + 'attachment/' +
                    h.urlquote(self.filename))
