import datetime

from django.contrib.postgres.fields import JSONField
from django.contrib.contenttypes.fields import GenericRelation
from django.utils import timezone
from django.db import models
from django.db.models import Q
from django.urls import reverse
from django.utils.functional import cached_property

from .constants import ASSIGNMENT_REFUSAL_REASONS, ASSIGNMENT_NULLBOOL,\
                       SUBMISSION_TYPE, ED_COMM_CHOICES, REFEREE_QUALIFICATION, QUALITY_SPEC,\
                       RANKING_CHOICES, REPORT_REC, SUBMISSION_STATUS, STATUS_UNASSIGNED,\
                       REPORT_STATUSES, STATUS_UNVETTED, SUBMISSION_EIC_RECOMMENDATION_REQUIRED,\
                       SUBMISSION_CYCLES, CYCLE_DEFAULT, CYCLE_SHORT, CYCLE_DIRECT_REC,\
                       EVENT_GENERAL, EVENT_TYPES, EVENT_FOR_AUTHOR, EVENT_FOR_EIC
from .managers import SubmissionManager, EditorialAssignmentManager, EICRecommendationManager,\
                      ReportQuerySet, SubmissionEventQuerySet
from .utils import ShortSubmissionCycle, DirectRecommendationSubmissionCycle,\
                   GeneralSubmissionCycle

from comments.models import Comment
from scipost.behaviors import TimeStampedModel
from scipost.constants import TITLE_CHOICES
from scipost.fields import ChoiceArrayField
from scipost.models import Contributor
from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS
from journals.constants import SCIPOST_JOURNALS_SUBMIT, SCIPOST_JOURNALS_DOMAINS
from journals.models import Publication


###############
# Submissions:
###############
class Submission(models.Model):
    # Main submission fields
    author_comments = models.TextField(blank=True, null=True)
    author_list = models.CharField(max_length=1000, verbose_name="author list")
    discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, default='physics')
    domain = models.CharField(max_length=3, choices=SCIPOST_JOURNALS_DOMAINS)
    editor_in_charge = models.ForeignKey('scipost.Contributor', related_name='EIC', blank=True,
                                         null=True, on_delete=models.CASCADE)
    is_current = models.BooleanField(default=True)
    is_resubmission = models.BooleanField(default=False)
    list_of_changes = models.TextField(blank=True, null=True)
    open_for_commenting = models.BooleanField(default=False)
    open_for_reporting = models.BooleanField(default=False)
    referees_flagged = models.TextField(blank=True, null=True)
    referees_suggested = models.TextField(blank=True, null=True)
    remarks_for_editors = models.TextField(blank=True, null=True)
    reporting_deadline = models.DateTimeField(default=timezone.now)
    secondary_areas = ChoiceArrayField(
        models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS),
        blank=True, null=True)

    # Status set by Editors
    status = models.CharField(max_length=30, choices=SUBMISSION_STATUS, default=STATUS_UNASSIGNED)
    refereeing_cycle = models.CharField(max_length=30, choices=SUBMISSION_CYCLES,
                                        default=CYCLE_DEFAULT)
    subject_area = models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS,
                                    verbose_name='Primary subject area', default='Phys:QP')
    submission_type = models.CharField(max_length=10, choices=SUBMISSION_TYPE,
                                       blank=True, null=True, default=None)
    submitted_by = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE)

    # Replace this by foreignkey?
    submitted_to_journal = models.CharField(max_length=30, choices=SCIPOST_JOURNALS_SUBMIT,
                                            verbose_name="Journal to be submitted to")
    title = models.CharField(max_length=300)

    # Authors which have been mapped to contributors:
    authors = models.ManyToManyField('scipost.Contributor', blank=True, related_name='authors_sub')
    authors_claims = models.ManyToManyField('scipost.Contributor', blank=True,
                                            related_name='authors_sub_claims')
    authors_false_claims = models.ManyToManyField('scipost.Contributor', blank=True,
                                                  related_name='authors_sub_false_claims')
    abstract = models.TextField()

    # Comments can be added to a Submission
    comments = GenericRelation('comments.Comment', related_query_name='submissions')

    # Arxiv identifiers with/without version number
    arxiv_identifier_w_vn_nr = models.CharField(max_length=15, default='0000.00000v0')
    arxiv_identifier_wo_vn_nr = models.CharField(max_length=10, default='0000.00000')
    arxiv_vn_nr = models.PositiveSmallIntegerField(default=1)
    arxiv_link = models.URLField(verbose_name='arXiv link (including version nr)')

    pdf_refereeing_pack = models.FileField(upload_to='UPLOADS/REFEREE/%Y/%m/',
                                           max_length=200, blank=True)

    # Metadata
    metadata = JSONField(default={}, blank=True, null=True)
    submission_date = models.DateField(verbose_name='submission date', default=datetime.date.today)
    acceptance_date = models.DateField(verbose_name='acceptance date', null=True, blank=True)
    latest_activity = models.DateTimeField(auto_now=True)

    objects = SubmissionManager()

    class Meta:
        permissions = (
            ('can_take_editorial_actions', 'Can take editorial actions'),
        )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._update_cycle()

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        self._update_cycle()

    def __str__(self):
        header = (self.arxiv_identifier_w_vn_nr + ', '
                  + self.title[:30] + ' by ' + self.author_list[:30])
        if self.is_current:
            header += ' (current version)'
        else:
            header += ' (deprecated version ' + str(self.arxiv_vn_nr) + ')'
        try:
            header += ' (published as %s (%s))' % (self.publication.doi_string,
                                                   self.publication.publication_date.strftime('%Y'))
        except Publication.DoesNotExist:
            pass
        return header

    def comments_set_complete(self):
        """
        Return comments to Submission, comments on Reports of Submission and
        nested comments related to this Submission.
        """
        return Comment.objects.filter(Q(submissions=self) |
                                      Q(reports__submission=self) |
                                      Q(comments__reports__submission=self) |
                                      Q(comments__submissions=self)).distinct()

    def _update_cycle(self):
        """
        Append the specific submission cycle to the instance to eventually handle the
        complete submission cycle outside the submission instance itself.
        """
        if self.refereeing_cycle == CYCLE_SHORT:
            self.cycle = ShortSubmissionCycle(self)
        elif self.refereeing_cycle == CYCLE_DIRECT_REC:
            self.cycle = DirectRecommendationSubmissionCycle(self)
        else:
            self.cycle = GeneralSubmissionCycle(self)

    def get_absolute_url(self):
        return reverse('submissions:submission', args=[self.arxiv_identifier_w_vn_nr])

    def eic_recommendation_required(self):
        return self.status not in SUBMISSION_EIC_RECOMMENDATION_REQUIRED

    @property
    def reporting_deadline_has_passed(self):
        return timezone.now() > self.reporting_deadline

    @cached_property
    def other_versions(self):
        return Submission.objects.filter(
            arxiv_identifier_wo_vn_nr=self.arxiv_identifier_wo_vn_nr
        ).exclude(pk=self.id).order_by('-arxiv_vn_nr')

    # Underneath: All very inefficient methods as they initiate a new query
    def count_accepted_invitations(self):
        return self.referee_invitations.filter(accepted=True).count()

    def count_declined_invitations(self):
        return self.referee_invitations.filter(accepted=False).count()

    def count_pending_invitations(self):
        return self.referee_invitations.filter(accepted=None).count()

    def count_invited_reports(self):
        return self.reports.accepted().filter(invited=True).count()

    def count_contrib_reports(self):
        return self.reports.accepted().filter(invited=False).count()

    def count_obtained_reports(self):
        return self.reports.accepted().filter(invited__isnull=False).count()

    def add_general_event(self, message):
        event = SubmissionEvent(
            submission=self,
            event=EVENT_GENERAL,
            text=message,
        )
        event.save()

    def add_event_for_author(self, message):
        event = SubmissionEvent(
            submission=self,
            event=EVENT_FOR_AUTHOR,
            text=message,
        )
        event.save()

    def add_event_for_eic(self, message):
        event = SubmissionEvent(
            submission=self,
            event=EVENT_FOR_EIC,
            text=message,
        )
        event.save()


class SubmissionEvent(TimeStampedModel):
    """
    The SubmissionEvent's goal is to act as a messaging/logging model
    for the Submission cycle. Its main audience will be the author(s) and
    the Editor-in-charge of a Submission.

    Be aware!
    Both the author and editor-in-charge will read the submission event.
    Make sure the right text is given to the right event-type, to protect
    the fellow's identity.
    """
    submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE,
                                   related_name='events')
    event = models.CharField(max_length=4, choices=EVENT_TYPES, default=EVENT_GENERAL)
    text = models.TextField()

    objects = SubmissionEventQuerySet.as_manager()

    class Meta:
        ordering = ['-created']

    def __str__(self):
        return '%s: %s' % (str(self.submission), self.get_event_display())


######################
# Editorial workflow #
######################

class EditorialAssignment(models.Model):
    submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE)
    to = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE)
    accepted = models.NullBooleanField(choices=ASSIGNMENT_NULLBOOL, default=None)
    # attribute `deprecated' becomes True if another Fellow becomes Editor-in-charge
    deprecated = models.BooleanField(default=False)
    completed = models.BooleanField(default=False)
    refusal_reason = models.CharField(max_length=3, choices=ASSIGNMENT_REFUSAL_REASONS,
                                      blank=True, null=True)
    date_created = models.DateTimeField(default=timezone.now)
    date_answered = models.DateTimeField(blank=True, null=True)

    objects = EditorialAssignmentManager()

    def __str__(self):
        return (self.to.user.first_name + ' ' + self.to.user.last_name + ' to become EIC of ' +
                self.submission.title[:30] + ' by ' + self.submission.author_list[:30] +
                ', requested on ' + self.date_created.strftime('%Y-%m-%d'))


class RefereeInvitation(models.Model):
    submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE,
                                   related_name='referee_invitations')
    referee = models.ForeignKey('scipost.Contributor', related_name='referee', blank=True,
                                null=True, on_delete=models.CASCADE)  # Why is this blank/null=True
    title = models.CharField(max_length=4, choices=TITLE_CHOICES)
    first_name = models.CharField(max_length=30, default='')
    last_name = models.CharField(max_length=30, default='')
    email_address = models.EmailField()
    # if Contributor not found, person is invited to register
    invitation_key = models.CharField(max_length=40, default='')
    date_invited = models.DateTimeField(default=timezone.now)
    invited_by = models.ForeignKey('scipost.Contributor', related_name='referee_invited_by',
                                   blank=True, null=True, on_delete=models.CASCADE)
    nr_reminders = models.PositiveSmallIntegerField(default=0)
    date_last_reminded = models.DateTimeField(blank=True, null=True)
    accepted = models.NullBooleanField(choices=ASSIGNMENT_NULLBOOL, default=None)
    date_responded = models.DateTimeField(blank=True, null=True)
    refusal_reason = models.CharField(max_length=3, choices=ASSIGNMENT_REFUSAL_REASONS,
                                      blank=True, null=True)
    fulfilled = models.BooleanField(default=False)  # True if a Report has been submitted
    cancelled = models.BooleanField(default=False)  # True if EIC has deactivated invitation

    def __str__(self):
        return (self.first_name + ' ' + self.last_name + ' to referee ' +
                self.submission.title[:30] + ' by ' + self.submission.author_list[:30] +
                ', invited on ' + self.date_invited.strftime('%Y-%m-%d'))

    @property
    def referee_str(self):
        if self.referee:
            return str(self.referee)
        return self.last_name + ', ' + self.first_name

    def reset_content(self):
        self.nr_reminders = 0
        self.date_last_reminded = None
        self.accepted = None
        self.refusal_reason = None
        self.fulfilled = False
        self.cancelled = False


###########
# Reports:
###########

class Report(models.Model):
    """
    Both types of reports, invited or contributed.

    This Report model acts as both a regular `Report` and a `FollowupReport`; A normal Report
    should have all fields required, whereas a FollowupReport only has the `report` field as
    a required field.

    Important note!
    Due to the construction of the two different types within a single model, it is important
    to explicitly implement the perticular differences in for example the form used.
    """
    status = models.CharField(max_length=16, choices=REPORT_STATUSES, default=STATUS_UNVETTED)
    submission = models.ForeignKey('submissions.Submission', related_name='reports',
                                   on_delete=models.CASCADE)
    report_nr = models.PositiveSmallIntegerField(default=0,
                                                 help_text='This number is a unique number '
                                                           'refeering to the Report nr. of '
                                                           'the Submission')
    vetted_by = models.ForeignKey('scipost.Contributor', related_name="report_vetted_by",
                                  blank=True, null=True, on_delete=models.CASCADE)

    # `invited' filled from RefereeInvitation objects at moment of report submission
    invited = models.BooleanField(default=False)

    # `flagged' if author of report has been flagged by submission authors (surname check only)
    flagged = models.BooleanField(default=False)
    date_submitted = models.DateTimeField('date submitted')
    author = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE)
    qualification = models.PositiveSmallIntegerField(
        choices=REFEREE_QUALIFICATION,
        verbose_name="Qualification to referee this: I am")

    # Text-based reporting
    strengths = models.TextField(blank=True)
    weaknesses = models.TextField(blank=True)
    report = models.TextField()
    requested_changes = models.TextField(verbose_name="requested changes", blank=True)

    # Comments can be added to a Submission
    comments = GenericRelation('comments.Comment', related_query_name='reports')

    # Qualities:
    validity = models.PositiveSmallIntegerField(choices=RANKING_CHOICES,
                                                null=True, blank=True)
    significance = models.PositiveSmallIntegerField(choices=RANKING_CHOICES,
                                                    null=True, blank=True)
    originality = models.PositiveSmallIntegerField(choices=RANKING_CHOICES,
                                                   null=True, blank=True)
    clarity = models.PositiveSmallIntegerField(choices=RANKING_CHOICES,
                                               null=True, blank=True)
    formatting = models.SmallIntegerField(choices=QUALITY_SPEC, null=True, blank=True,
                                          verbose_name="Quality of paper formatting")
    grammar = models.SmallIntegerField(choices=QUALITY_SPEC, null=True, blank=True,
                                       verbose_name="Quality of English grammar")

    recommendation = models.SmallIntegerField(choices=REPORT_REC)
    remarks_for_editors = models.TextField(blank=True,
                                           verbose_name='optional remarks for the Editors only')
    doi_label = models.CharField(max_length=200, blank=True)
    anonymous = models.BooleanField(default=True, verbose_name='Publish anonymously')
    pdf_report = models.FileField(upload_to='UPLOADS/REPORTS/%Y/%m/', max_length=200, blank=True)

    objects = ReportQuerySet.as_manager()

    class Meta:
        unique_together = ('submission', 'report_nr')
        default_related_name = 'reports'
        ordering = ['-date_submitted']
        permissions = (
            ('can_vet_submitted_reports', 'Can vet submitted Reports'),
        )

    def __str__(self):
        return (self.author.user.first_name + ' ' + self.author.user.last_name + ' on ' +
                self.submission.title[:50] + ' by ' + self.submission.author_list[:50])

    def save(self, *args, **kwargs):
        # Control Report count per Submission.
        if not self.report_nr:
            self.report_nr = self.submission.reports.count() + 1
        return super().save(*args, **kwargs)

    def get_absolute_url(self):
        return self.submission.get_absolute_url() + '#report_' + str(self.report_nr)

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

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

    @cached_property
    def is_followup_report(self):
        """
        Check if current Report is a `FollowupReport`. A Report is a `FollowupReport` if the
        author of the report already has a vetted report in the series of the specific Submission.
        """
        return (self.author.reports.accepted()
                .filter(submission__arxiv_identifier_wo_vn_nr=self.submission.arxiv_identifier_wo_vn_nr)
                .exists())

    def latest_report_from_series(self):
        """
        Get latest Report from the same author for the Submission series.
        """
        return (self.author.reports.accepted()
                .filter(submission__arxiv_identifier_wo_vn_nr=self.submission.arxiv_identifier_wo_vn_nr)
                .order_by('submission__arxiv_identifier_wo_vn_nr').last())


##########################
# EditorialCommunication #
##########################

class EditorialCommunication(models.Model):
    """
    Each individual communication between Editor-in-charge
    to and from Referees and Authors becomes an instance of this class.
    """
    submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE,
                                   related_name='editorial_communications')
    referee = models.ForeignKey('scipost.Contributor', related_name='referee_in_correspondence',
                                blank=True, null=True, on_delete=models.CASCADE)
    comtype = models.CharField(max_length=4, choices=ED_COMM_CHOICES)
    timestamp = models.DateTimeField(default=timezone.now)
    text = models.TextField()

    class Meta:
        ordering = ['timestamp']

    def __str__(self):
        output = self.comtype
        if self.referee is not None:
            output += ' ' + self.referee.user.first_name + ' ' + self.referee.user.last_name
        output += (' for submission ' + self.submission.title[:30] + ' by '
                   + self.submission.author_list[:30])
        return output


############################
# Editorial Recommendation #
############################

# From the Editor-in-charge of a Submission
class EICRecommendation(models.Model):
    submission = models.ForeignKey('submissions.Submission', on_delete=models.CASCADE,
                                   related_name='eicrecommendations')
    date_submitted = models.DateTimeField('date submitted', default=timezone.now)
    remarks_for_authors = models.TextField(blank=True, null=True)
    requested_changes = models.TextField(verbose_name="requested changes", blank=True, null=True)
    remarks_for_editorial_college = models.TextField(blank=True,
                                                     verbose_name='optional remarks for the'
                                                                  ' Editorial College')
    recommendation = models.SmallIntegerField(choices=REPORT_REC)

    # Editorial Fellows who have assessed this recommendation:
    eligible_to_vote = models.ManyToManyField(Contributor, blank=True,
                                              related_name='eligible_to_vote')
    voted_for = models.ManyToManyField(Contributor, blank=True, related_name='voted_for')
    voted_against = models.ManyToManyField(Contributor, blank=True, related_name='voted_against')
    voted_abstain = models.ManyToManyField(Contributor, blank=True, related_name='voted_abstain')
    voting_deadline = models.DateTimeField('date submitted', default=timezone.now)

    objects = EICRecommendationManager()

    def __str__(self):
        return (self.submission.title[:20] + ' by ' + self.submission.author_list[:30] +
                ', ' + self.get_recommendation_display())

    @property
    def nr_for(self):
        return self.voted_for.count()

    @property
    def nr_against(self):
        return self.voted_against.count()

    @property
    def nr_abstained(self):
        return self.voted_abstain.count()