from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.functional import cached_property
from django.urls import reverse

from guardian.shortcuts import assign_perm

from scipost.behaviors import TimeStampedModel
from scipost.models import Contributor
from commentaries.constants import COMMENTARY_PUBLISHED

from .behaviors import validate_file_extension, validate_max_file_size
from .constants import COMMENT_STATUS, STATUS_PENDING
from .managers import CommentQuerySet


WARNING_TEXT = 'Warning: Rather use/edit `content_object` instead or be 100% sure you know what you are doing!'
US_NOTICE = 'Warning: This field is out of service and will be removed in the future.'


class Comment(TimeStampedModel):
    """ A Comment is an unsollicited note, submitted by a Contributor,
    on a particular publication or in reply to an earlier Comment. """

    status = models.SmallIntegerField(default=STATUS_PENDING, choices=COMMENT_STATUS)
    vetted_by = models.ForeignKey('scipost.Contributor', blank=True, null=True,
                                  on_delete=models.CASCADE, related_name='comment_vetted_by')
    file_attachment = models.FileField(upload_to='uploads/comments/%Y/%m/%d/', blank=True,
                                       validators=[validate_file_extension, validate_max_file_size]
                                       )

    # A Comment is always related to another model
    # This construction implicitly has property: `on_delete=models.CASCADE`
    content_type = models.ForeignKey(ContentType, help_text=WARNING_TEXT)
    object_id = models.PositiveIntegerField(help_text=WARNING_TEXT)
    content_object = GenericForeignKey()

    nested_comments = GenericRelation('comments.Comment', related_query_name='comments')

    # -- U/S
    # These fields will be removed in the future.
    # They still exists only to prevent possible data loss.
    commentary = models.ForeignKey('commentaries.Commentary', blank=True, null=True,
                                   on_delete=models.CASCADE, help_text=US_NOTICE)
    submission = models.ForeignKey('submissions.Submission', blank=True, null=True,
                                   on_delete=models.CASCADE, related_name='comments_old',
                                   help_text=US_NOTICE)
    thesislink = models.ForeignKey('theses.ThesisLink', blank=True, null=True,
                                   on_delete=models.CASCADE, help_text=US_NOTICE)
    in_reply_to_comment = models.ForeignKey('self', blank=True, null=True,
                                            related_name="nested_comments_old",
                                            on_delete=models.CASCADE, help_text=US_NOTICE)
    in_reply_to_report = models.ForeignKey('submissions.Report', blank=True, null=True,
                                           on_delete=models.CASCADE, help_text=US_NOTICE)
    # -- End U/S

    # Author info
    is_author_reply = models.BooleanField(default=False)
    author = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE,
                               related_name='comments')
    anonymous = models.BooleanField(default=False, verbose_name='Publish anonymously')

    # Categories:
    is_cor = models.BooleanField(default=False, verbose_name='correction/erratum')
    is_rem = models.BooleanField(default=False, verbose_name='remark')
    is_que = models.BooleanField(default=False, verbose_name='question')
    is_ans = models.BooleanField(default=False, verbose_name='answer to question')
    is_obj = models.BooleanField(default=False, verbose_name='objection')
    is_rep = models.BooleanField(default=False, verbose_name='reply to objection')
    is_val = models.BooleanField(default=False, verbose_name='validation or rederivation')
    is_lit = models.BooleanField(default=False, verbose_name='pointer to related literature')
    is_sug = models.BooleanField(default=False, verbose_name='suggestion for further work')
    comment_text = models.TextField()
    remarks_for_editors = models.TextField(blank=True,
                                           verbose_name='optional remarks for the Editors only')
    date_submitted = models.DateTimeField('date submitted', default=timezone.now)
    # Opinions
    nr_A = models.PositiveIntegerField(default=0)
    in_agreement = models.ManyToManyField('scipost.Contributor', related_name='in_agreement',
                                          blank=True)
    nr_N = models.PositiveIntegerField(default=0)
    in_notsure = models.ManyToManyField('scipost.Contributor', related_name='in_notsure',
                                        blank=True)
    nr_D = models.PositiveIntegerField(default=0)
    in_disagreement = models.ManyToManyField('scipost.Contributor', related_name='in_disagreement',
                                             blank=True)

    needs_doi = models.NullBooleanField(default=None)
    doideposit_needs_updating = models.BooleanField(default=False)
    genericdoideposit = GenericRelation('journals.GenericDOIDeposit',
                                        related_query_name='genericdoideposit')
    doi_label = models.CharField(max_length=200, blank=True)
    objects = CommentQuerySet.as_manager()

    class Meta:
        permissions = (
            ('can_vet_comments', 'Can vet submitted Comments'),
        )

    def __str__(self):
        return ('by ' + self.author.user.first_name + ' ' + self.author.user.last_name +
                ' on ' + self.date_submitted.strftime('%Y-%m-%d') + ', ' + self.comment_text[:30])

    @property
    def title(self):
        """
        This property is (mainly) used to let Comments get the title of the Submission without
        annoying logic.
        """
        try:
            return self.content_object.title
        except:
            return self.content_type

    @cached_property
    def core_content_object(self):
        # Import here due to circular import errors
        from commentaries.models import Commentary
        from submissions.models import Submission, Report
        from theses.models import ThesisLink

        to_object = self.content_object
        while True:
            if (isinstance(to_object, Submission) or isinstance(to_object, Commentary) or
               isinstance(to_object, ThesisLink)):
                return to_object
            elif isinstance(to_object, Report):
                return to_object.submission
            elif isinstance(to_object, Comment):
                # Nested Comment.
                to_object = to_object.content_object
            else:
                raise Exception

    def create_doi_label(self):
        self.doi_label = 'SciPost.Comment.' + str(self.id)
        self.save()

    @property
    def doi_string(self):
        if self.doi_label:
            return '10.21468/' + self.doi_label
        else:
            return None

    def get_absolute_url(self):
        return self.content_object.get_absolute_url().split('#')[0] + '#comment_id' + str(self.id)

    def get_attachment_url(self):
        return reverse('comments:attachment', args=(self.id,))

    def grant_permissions(self):
        # Import here due to circular import errors
        from submissions.models import Submission

        to_object = self.core_content_object
        if isinstance(to_object, Submission):
            # Add permissions for EIC only, the Vetting-group already has it!
            assign_perm('comments.can_vet_comments', to_object.editor_in_charge.user, self)

    def get_author(self):
        '''Get author, if and only if comment is not anonymous!!!'''
        if not self.anonymous:
            return self.author
        return None

    def get_author_str(self):
        '''Get author string, if and only if comment is not anonymous!!!'''
        author = self.get_author()
        if author:
            return author.user.first_name + ' ' + author.user.last_name
        return 'Anonymous'

    def update_opinions(self, contributor_id, opinion):
        contributor = get_object_or_404(Contributor, pk=contributor_id)
        self.in_agreement.remove(contributor)
        self.in_notsure.remove(contributor)
        self.in_disagreement.remove(contributor)
        if opinion == 'A':
            self.in_agreement.add(contributor)
        elif opinion == 'N':
            self.in_notsure.add(contributor)
        elif opinion == 'D':
            self.in_disagreement.add(contributor)
        self.nr_A = self.in_agreement.count()
        self.nr_N = self.in_notsure.count()
        self.nr_D = self.in_disagreement.count()
        self.save()

    @property
    def relation_to_published(self):
        """
        Check if the Comment relates to a SciPost-published object.
        If it is, return a dict with info on relation to the published object,
        based on Crossref's peer review content type.
        """
        # Import here due to circular import errors
        from submissions.models import Submission
        from journals.models import Publication
        from commentaries.models import Commentary

        to_object = self.core_content_object
        if isinstance(to_object, Submission):
            publication = Publication.objects.filter(
                accepted_submission__arxiv_identifier_wo_vn_nr=to_object.arxiv_identifier_wo_vn_nr)
            if publication:
                relation = {
                    'isReviewOfDOI': publication.doi_string,
                    'stage': 'pre-publication',
                    'title': 'Comment on ' + to_object.arxiv_identifier_w_vn_nr,
                }
                if self.is_author_reply:
                    relation['type'] = 'author-comment'
                else:
                    relation['type'] = 'community-comment'
                return relation
        if isinstance(to_object, Commentary):
            if to_object.type == COMMENTARY_PUBLISHED:
                relation = {
                    'isReviewOfDOI': to_object.pub_doi,
                    'stage': 'post-publication',
                    'title': 'Comment on ' + to_object.pub_doi,
                }
                if self.is_author_reply:
                    relation['type'] = 'author-comment'
                    relation['contributor_role'] = 'author'
                else:
                    relation['type'] = 'community-comment'
                    relation['contributor_role'] = 'reviewer-external'
                return relation

        return None

    @property
    def citation(self):
        citation = ''
        if self.doi_string:
            if self.anonymous:
                citation += 'Anonymous, '
            else:
                citation += '%s %s, ' % (self.author.user.first_name, self.author.user.last_name)

            if self.is_author_reply:
                citation += 'SciPost Author Replies, '
            else:
                citation += 'SciPost Comments, '
            citation += 'Delivered %s, ' % self.date_submitted.strftime('%Y-%m-%d')
            citation += 'doi: %s' % self.doi_string
        return citation