__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"


import datetime
import re

from django import forms
from django.conf import settings
# from django.contrib.postgres.search import TrigramSimilarity
from django.db import transaction
from django.db.models import Q
from django.forms.formsets import ORDERING_FIELD_NAME
from django.utils import timezone

from .constants import (
    ASSIGNMENT_BOOL, ASSIGNMENT_REFUSAL_REASONS, STATUS_RESUBMITTED, REPORT_ACTION_CHOICES,
    REPORT_REFUSAL_CHOICES, STATUS_REJECTED, STATUS_INCOMING, REPORT_POST_EDREC, REPORT_NORMAL,
    STATUS_DRAFT, STATUS_UNVETTED, REPORT_ACTION_ACCEPT, REPORT_ACTION_REFUSE, STATUS_UNASSIGNED,
    EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS, SUBMISSION_STATUS, PUT_TO_VOTING, CYCLE_UNDETERMINED,
    SUBMISSION_CYCLE_CHOICES, REPORT_PUBLISH_1, REPORT_PUBLISH_2, REPORT_PUBLISH_3, STATUS_VETTED,
    REPORT_MINOR_REV, REPORT_MAJOR_REV, REPORT_REJECT, DECISION_FIXED, DEPRECATED, STATUS_COMPLETED,
    STATUS_EIC_ASSIGNED, CYCLE_DEFAULT, CYCLE_DIRECT_REC, STATUS_PREASSIGNED, STATUS_REPLACED,
    STATUS_FAILED_PRESCREENING, STATUS_DEPRECATED, STATUS_ACCEPTED, STATUS_DECLINED, STATUS_WITHDRAWN)
from . import exceptions, helpers
from .helpers import to_ascii_only
from .models import (
    Submission, RefereeInvitation, Report, EICRecommendation, EditorialAssignment,
    iThenticateReport, EditorialCommunication)
from .signals import notify_manuscript_accepted

from colleges.models import Fellowship
from journals.models import Journal
from journals.constants import SCIPOST_JOURNAL_PHYSICS_PROC, SCIPOST_JOURNAL_PHYSICS
from mails.utils import DirectMailUtil
from preprints.helpers import generate_new_scipost_identifier
from preprints.models import Preprint
from production.utils import get_or_create_production_stream
from profiles.models import Profile
from scipost.constants import SCIPOST_SUBJECT_AREAS
from scipost.services import ArxivCaller
from scipost.models import Contributor, Remark
import strings

import iThenticate

IDENTIFIER_PATTERN_NEW = r'^[0-9]{4,}\.[0-9]{4,5}v[0-9]{1,2}$'


class SubmissionSearchForm(forms.Form):
    """Filter a Submission queryset using basic search fields."""

    author = forms.CharField(max_length=100, required=False, label="Author(s)")
    title = forms.CharField(max_length=100, required=False)
    abstract = forms.CharField(max_length=1000, required=False)
    subject_area = forms.CharField(max_length=10, required=False, widget=forms.Select(
                                   choices=((None, 'Show all'),) + SCIPOST_SUBJECT_AREAS[0][1]))

    def search_results(self):
        """Return all Submission objects according to search."""
        return Submission.objects.public_newest().filter(
            title__icontains=self.cleaned_data.get('title', ''),
            author_list__icontains=self.cleaned_data.get('author', ''),
            abstract__icontains=self.cleaned_data.get('abstract', ''),
            subject_area__icontains=self.cleaned_data.get('subject_area', '')
        )


class SubmissionPoolFilterForm(forms.Form):
    status = forms.ChoiceField(
        choices=((None, 'All submissions currently under evaluation'),) + SUBMISSION_STATUS,
        required=False)
    editor_in_charge = forms.BooleanField(
        label='Show only Submissions for which I am editor in charge.', required=False)

    def search(self, queryset, current_user):
        if self.cleaned_data.get('status'):
            # Do extra check on non-required field to never show errors on template
            queryset = queryset.pool_editable(current_user).filter(
                status=self.cleaned_data['status'])
        else:
            # If no specific status if requested, just return the Pool by default
            queryset = queryset.pool(current_user)

        if self.cleaned_data.get('editor_in_charge') and hasattr(current_user, 'contributor'):
            queryset = queryset.filter(editor_in_charge=current_user.contributor)

        return queryset.order_by('-submission_date')

    def status_verbose(self):
        try:
            return dict(SUBMISSION_STATUS)[self.cleaned_data['status']]
        except KeyError:
            return ''


###############################
# Submission and resubmission #
###############################

class SubmissionService:
    """
    Object to run checks for prefiller and submit manuscript forms.
    """

    metadata = {}

    def __init__(self, requested_by, preprint_server, identifier=None, resubmission_of_id=None):
        self.requested_by = requested_by
        self.preprint_server = preprint_server
        self.identifier = identifier
        self.resubmission_of_id = resubmission_of_id
        self._arxiv_data = None

    @property
    def latest_submission(self):
        """
        Return latest version of preprint series or None.
        """
        if hasattr(self, '_latest_submission'):
            return self._latest_submission

        if self.identifier:
            # Check if is resubmission when identifier data is submitted.
            identifier = self.identifier.rpartition('v')[0]
            self._latest_submission = Submission.objects.filter(
                preprint__identifier_wo_vn_nr=identifier).order_by(
                '-preprint__vn_nr').first()
        elif self.resubmission_of_id:
            # Resubmission (submission id) is selected by user.
            try:
                self._latest_submission = Submission.objects.filter(
                    id=int(self.resubmission_of_id)).order_by('-preprint__vn_nr').first()
            except ValueError:
                self._latest_submission = None
        else:
            self._latest_submission = None
        return self._latest_submission

    @property
    def arxiv_data(self):
        if self._arxiv_data is None:
            self._call_arxiv()
        return self._arxiv_data

    def run_checks(self):
        """
        Do several pre-checks (using the arXiv API if needed).

        This is needed for both the prefill and submission forms.
        """
        self._submission_already_exists()
        self._submission_previous_version_is_valid_for_submission()

        if self.preprint_server == 'arxiv':
            self._submission_is_already_published()

    def _call_arxiv(self):
        """
        Retrieve all data from the ArXiv database for `identifier`.
        """
        if self.preprint_server != 'arxiv':
            # Do the call here to prevent multiple calls to the arXiv API in one request.
            self._arxiv_data = {}
            return
        if not self.identifier:
            print('crap', self.identifier)
            return

        caller = ArxivCaller(self.identifier)

        if caller.is_valid:
            self._arxiv_data = caller.data
            self.metadata = caller.metadata
        else:
            error_message = 'A preprint associated to this identifier does not exist.'
            raise forms.ValidationError(error_message)

    def get_latest_submission_data(self):
        """
        Return initial form data originating from earlier Submission.
        """
        if self.is_resubmission():
            return {
                'title': self.latest_submission.title,
                'abstract': self.latest_submission.abstract,
                'author_list': self.latest_submission.author_list,
                'discipline': self.latest_submission.discipline,
                'domain': self.latest_submission.domain,
                'referees_flagged': self.latest_submission.referees_flagged,
                'referees_suggested': self.latest_submission.referees_suggested,
                'secondary_areas': self.latest_submission.secondary_areas,
                'subject_area': self.latest_submission.subject_area,
                'submitted_to': self.latest_submission.submitted_to,
                'submission_type': self.latest_submission.submission_type,
            }
        return {}

    def is_resubmission(self):
        """
        Check if Submission is a SciPost or arXiv resubmission.
        """
        return self.latest_submission is not None

    def identifier_matches_regex(self, journal_code):
        """
        Check if identifier is valid for the Journal submitting to.
        """
        if self.preprint_server != 'arxiv':
            # Only check arXiv identifiers
            return

        if journal_code in EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS.keys():
            regex = EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS[journal_code]
        else:
            regex = EXPLICIT_REGEX_MANUSCRIPT_CONSTRAINTS['default']

        pattern = re.compile(regex)
        if not pattern.match(self.identifier):
            # No match object returned, identifier is invalid
            error_message = ('The journal you want to submit to does not allow for this'
                             ' identifier. Please contact SciPost if you have'
                             ' any further questions.')
            raise forms.ValidationError(error_message, code='submitted_to')

    def process_resubmission_procedure(self, submission):
        """
        Update all fields for new and old Submission and EditorialAssignments to comply with
        the resubmission procedures.

        -- submission: the new version of the Submission series.
        """
        if not self.latest_submission:
            raise Submission.DoesNotExist

        # Close last submission
        Submission.objects.filter(id=self.latest_submission.id).update(
            is_current=False, open_for_reporting=False, status=STATUS_RESUBMITTED)

        # Copy Topics
        submission.topics.add(*self.latest_submission.topics.all())

        # Open for comment and reporting and copy EIC info
        Submission.objects.filter(id=submission.id).update(
            open_for_reporting=True,
            open_for_commenting=True,
            is_resubmission_of=self.latest_submission,
            visible_pool=True,
            refereeing_cycle=CYCLE_UNDETERMINED,
            editor_in_charge=self.latest_submission.editor_in_charge,
            status=STATUS_EIC_ASSIGNED,
            thread_hash=self.latest_submission.thread_hash)

        # Add author(s) (claim) fields
        submission.authors.add(*self.latest_submission.authors.all())
        submission.authors_claims.add(*self.latest_submission.authors_claims.all())
        submission.authors_false_claims.add(*self.latest_submission.authors_false_claims.all())

        # Create new EditorialAssigment for the current Editor-in-Charge
        EditorialAssignment.objects.create(
            submission=submission,
            to=self.latest_submission.editor_in_charge,
            status=STATUS_ACCEPTED)

    def _submission_already_exists(self):
        """
        Check if preprint has already been submitted before.
        """
        if Submission.objects.filter(preprint__identifier_w_vn_nr=self.identifier).exists():
            error_message = 'This preprint version has already been submitted to SciPost.'
            raise forms.ValidationError(error_message, code='duplicate')

    def _submission_previous_version_is_valid_for_submission(self):
        """
        Check if previous submitted versions have the appropriate status.
        """

        if self.latest_submission:
            if self.latest_submission.status == STATUS_REJECTED:
                # Explicitly give rejected status warning.
                error_message = ('This preprint has previously undergone refereeing '
                                 'and has been rejected. Resubmission is only possible '
                                 'if the manuscript has been substantially reworked into '
                                 'a new submission with distinct identifier.')
                raise forms.ValidationError(error_message)
            elif self.latest_submission.open_for_resubmission:
                # Check if verified author list contains current user.
                if self.requested_by.contributor not in self.latest_submission.authors.all():
                    error_message = ('There exists a preprint with this identifier '
                                     'but an earlier version number. Resubmission is only possible'
                                     ' if you are a registered author of this manuscript.')
                    raise forms.ValidationError(error_message)
            else:
                # Submission has not an appropriate status for resubmission.
                error_message = ('There exists a preprint with this identifier '
                                 'but an earlier version number, which is still undergoing '
                                 'peer refereeing. '
                                 'A resubmission can only be performed after request '
                                 'from the Editor-in-charge. Please wait until the '
                                 'closing of the previous refereeing round and '
                                 'formulation of the Editorial Recommendation '
                                 'before proceeding with a resubmission.')
                raise forms.ValidationError(error_message)

    def _submission_is_already_published(self):
        """
        Check if preprint number is already registered with a DOI in the *ArXiv* database.
        """
        published_id = None
        if 'arxiv_doi' in self.arxiv_data:
            published_id = self.arxiv_data['arxiv_doi']
        elif 'arxiv_journal_ref' in self.arxiv_data:
            published_id = self.arxiv_data['arxiv_journal_ref']

        if published_id:
            error_message = ('This paper has been published under DOI %(published_id)s'
                             '. Please comment on the published version.'),
            raise forms.ValidationError(error_message, code='published',
                                        params={'published_id': published_id})


class SubmissionForm(forms.ModelForm):
    """
    Form to submit a new (re)Submission.
    """

    identifier_w_vn_nr = forms.CharField(widget=forms.HiddenInput())
    preprint_file = forms.FileField()

    class Meta:
        model = Submission
        fields = [
            'is_resubmission_of',
            'discipline',
            'submitted_to',
            'proceedings',
            'submission_type',
            'domain',
            'subject_area',
            'secondary_areas',
            'title',
            'author_list',
            'abstract',
            'author_comments',
            'list_of_changes',
            'remarks_for_editors',
            'referees_suggested',
            'referees_flagged',
            'arxiv_link',
        ]
        widgets = {
            'is_resubmission_of': forms.HiddenInput(),
            'secondary_areas': forms.SelectMultiple(choices=SCIPOST_SUBJECT_AREAS),
            'arxiv_link': forms.TextInput(
                attrs={'placeholder': 'ex.:  arxiv.org/abs/1234.56789v1'}),
            'remarks_for_editors': forms.Textarea(
                attrs={'placeholder': 'Any private remarks (for the editors only)', 'rows': 5}),
            'referees_suggested': forms.Textarea(
                attrs={'placeholder': 'Optional: names of suggested referees', 'rows': 5}),
            'referees_flagged': forms.Textarea(
                attrs={
                    'placeholder': 'Optional: names of referees whose reports should be treated with caution (+ short reason)',
                    'rows': 5
                }),
            'author_comments': forms.Textarea(
                attrs={'placeholder': 'Your resubmission letter (will be viewable online)'}),
            'list_of_changes': forms.Textarea(
                attrs={'placeholder': 'Give a point-by-point list of changes (will be viewable online)'}),
        }

    def __init__(self, *args, **kwargs):
        self.requested_by = kwargs.pop('requested_by')
        self.preprint_server = kwargs.pop('preprint_server', 'arxiv')
        self.resubmission_preprint = kwargs['initial'].get('resubmission', False)

        data = args[0] if len(args) > 1 else kwargs.get('data', {})
        identifier = kwargs['initial'].get('identifier_w_vn_nr', None) or data.get('identifier_w_vn_nr')

        self.service = SubmissionService(
            self.requested_by, self.preprint_server,
            identifier=identifier,
            resubmission_of_id=self.resubmission_preprint)
        if self.preprint_server == 'scipost':
            kwargs['initial'] = self.service.get_latest_submission_data()

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

        if not self.preprint_server == 'arxiv':
            # No arXiv-specific data required.
            del self.fields['identifier_w_vn_nr']
            del self.fields['arxiv_link']
        elif not self.preprint_server == 'scipost':
            # No need for a file upload if user is not using the SciPost preprint server.
            del self.fields['preprint_file']

        # Find all submission allowed to be resubmitted by current user.
        self.fields['is_resubmission_of'].queryset = Submission.objects.candidate_for_resubmission(
            self.requested_by)

        # Fill resubmission-dependent fields
        if self.is_resubmission():
            self.fields['is_resubmission_of'].initial = self.service.latest_submission
        else:
            # These fields are only available for resubmissions.
            del self.fields['author_comments']
            del self.fields['list_of_changes']

        if not self.fields['is_resubmission_of'].initial:
            # No intial nor submitted data found.
            del self.fields['is_resubmission_of']

        # Select Journal instances.
        self.fields['submitted_to'].queryset = Journal.objects.active()
        self.fields['submitted_to'].label = 'Journal: submit to'

        # Proceedings submission fields
        qs = self.fields['proceedings'].queryset.open_for_submission()
        self.fields['proceedings'].queryset = qs
        self.fields['proceedings'].empty_label = None
        if not qs.exists():
            # No proceedings issue to submit to, so adapt the form fields
            self.fields['submitted_to'].queryset = self.fields['submitted_to'].queryset.exclude(
                doi_label=SCIPOST_JOURNAL_PHYSICS_PROC)
            del self.fields['proceedings']

    def is_resubmission(self):
        return self.service.is_resubmission()

    def clean(self, *args, **kwargs):
        """
        Do all general checks for Submission.
        """
        cleaned_data = super().clean(*args, **kwargs)

        # SciPost preprints are auto-generated here.
        self.scipost_identifier = None
        if 'identifier_w_vn_nr' not in cleaned_data:
            self.service.identifier, self.scipost_identifier = generate_new_scipost_identifier(
                cleaned_data.get('is_resubmission_of', None))
            # Also copy to the form data
            self.cleaned_data['identifier_w_vn_nr'] = self.service.identifier

        # Run checks again to clean any possible human intervention and run checks again
        # with possibly newly generated identifier.
        self.service.run_checks()
        self.service.identifier_matches_regex(cleaned_data['submitted_to'].doi_label)

        if self.cleaned_data['submitted_to'].doi_label != SCIPOST_JOURNAL_PHYSICS_PROC:
            try:
                del self.cleaned_data['proceedings']
            except KeyError:
                # No proceedings returned to data
                return cleaned_data
        return cleaned_data

    def clean_author_list(self):
        """
        Check if author list matches the Contributor submitting.
        """
        author_list = self.cleaned_data['author_list']

        # Remove punctuation and convert to ASCII-only string.
        clean_author_name = to_ascii_only(self.requested_by.last_name)
        clean_author_list = to_ascii_only(author_list)

        if not clean_author_name in clean_author_list:
            error_message = ('Your name does not match that of any of the authors. '
                             'You are not authorized to submit this preprint.')
            self.add_error('author_list', error_message)
        return author_list

    def clean_submission_type(self):
        """
        Validate Submission type for the SciPost Physics journal.
        """
        submission_type = self.cleaned_data['submission_type']
        journal_doi_label = self.cleaned_data['submitted_to'].doi_label
        if journal_doi_label == SCIPOST_JOURNAL_PHYSICS and not submission_type:
            self.add_error('submission_type', 'Please specify the submission type.')
        return submission_type

    def set_pool(self, submission):
        """
        Set the default set of (guest) Fellows for this Submission.
        """
        qs = Fellowship.objects.active()
        fellows = qs.regular().filter(
            contributor__discipline=submission.discipline).return_active_for_submission(submission)
        submission.fellows.set(fellows)

        if submission.proceedings:
            # Add Guest Fellowships if the Submission is a Proceedings manuscript
            guest_fellows = qs.guests().filter(
                proceedings=submission.proceedings).return_active_for_submission(submission)
            submission.fellows.add(*guest_fellows)

    @transaction.atomic
    def save(self):
        """
        Create the new Submission and Preprint instances.
        """
        submission = super().save(commit=False)
        submission.submitted_by = self.requested_by.contributor

        # Save identifiers
        identifiers = self.cleaned_data['identifier_w_vn_nr'].rpartition('v')
        preprint, __ = Preprint.objects.get_or_create(
            identifier_w_vn_nr=self.cleaned_data['identifier_w_vn_nr'],
            identifier_wo_vn_nr=identifiers[0],
            vn_nr=identifiers[2],
            url=self.cleaned_data.get('arxiv_link', ''),
            scipost_preprint_identifier=self.scipost_identifier,
            _file=self.cleaned_data.get('preprint_file', None), )

        # Save metadata directly from ArXiv call without possible user interception
        submission.metadata = self.service.metadata
        submission.preprint = preprint

        submission.save()
        if self.is_resubmission():
            self.service.process_resubmission_procedure(submission)

        # Gather first known author and Fellows.
        submission.authors.add(self.requested_by.contributor)
        self.set_pool(submission)

        # Return latest version of the Submission. It could be outdated by now.
        submission.refresh_from_db()
        return submission


class SubmissionIdentifierForm(forms.Form):
    """
    Prefill SubmissionForm using this form that takes an arXiv ID only.
    """

    IDENTIFIER_PLACEHOLDER = 'new style (with version nr) ####.####(#)v#(#)'

    identifier_w_vn_nr = forms.RegexField(
        label='arXiv identifier with version number',
        regex=IDENTIFIER_PATTERN_NEW, strip=True,
        error_messages={'invalid': strings.arxiv_query_invalid},
        widget=forms.TextInput({'placeholder': IDENTIFIER_PLACEHOLDER}))

    def __init__(self, *args, **kwargs):
        self.requested_by = kwargs.pop('requested_by')
        return super().__init__(*args, **kwargs)


    def clean_identifier_w_vn_nr(self):
        """
        Do basic prechecks based on the arXiv ID only.
        """
        identifier = self.cleaned_data.get('identifier_w_vn_nr', None)

        self.service = SubmissionService(self.requested_by, 'arxiv', identifier=identifier)
        self.service.run_checks()
        return identifier

    def get_initial_submission_data(self):
        """
        Return dictionary to prefill `SubmissionForm`.
        """
        form_data = self.service.arxiv_data
        form_data['identifier_w_vn_nr'] = self.cleaned_data['identifier_w_vn_nr']
        if self.service.is_resubmission():
            form_data.update({
                'discipline': self.service.latest_submission.discipline,
                'domain': self.service.latest_submission.domain,
                'referees_flagged': self.service.latest_submission.referees_flagged,
                'referees_suggested': self.service.latest_submission.referees_suggested,
                'secondary_areas': self.service.latest_submission.secondary_areas,
                'subject_area': self.service.latest_submission.subject_area,
                'submitted_to': self.service.latest_submission.submitted_to,
                'submission_type': self.service.latest_submission.submission_type,
            })
        return form_data


class SubmissionReportsForm(forms.ModelForm):
    """Update refereeing pdf for Submission."""

    class Meta:
        model = Submission
        fields = ['pdf_refereeing_pack']


class PreassignEditorsForm(forms.ModelForm):
    """Preassign editors for incoming Submission."""

    assign = forms.BooleanField(required=False)
    to = forms.ModelChoiceField(
        queryset=Contributor.objects.none(), required=True, widget=forms.HiddenInput())

    class Meta:
        model = EditorialAssignment
        fields = ('to',)

    def __init__(self, *args, **kwargs):
        self.submission = kwargs.pop('submission')
        super().__init__(*args, **kwargs)
        self.fields['to'].queryset = Contributor.objects.filter(
            fellowships__in=self.submission.fellows.all())
        self.fields['assign'].initial = self.instance.id is not None

    def save(self, commit=True):
        """Create/get unordered EditorialAssignments or delete existing if needed."""
        if self.cleaned_data['assign']:
            # Create/save
            self.instance, __ = EditorialAssignment.objects.get_or_create(
                submission=self.submission, to=self.cleaned_data['to'])
        elif self.instance.id is not None:
            # Delete if exists
            if self.instance.status == STATUS_PREASSIGNED:
                self.instance.delete()
        return self.instance

    def get_fellow(self):
        """Get fellow either via initial data or instance."""
        if self.instance.id is not None:
            return self.instance.to
        return self.initial.get('to', None)


class BasePreassignEditorsFormSet(forms.BaseModelFormSet):
    """Preassign editors for incoming Submission."""

    def __init__(self, *args, **kwargs):
        self.submission = kwargs.pop('submission')
        super().__init__(*args, **kwargs)
        self.queryset = self.submission.editorial_assignments.order_by('invitation_order')

        # Prefill form fields and create unassigned rows for unassigned fellows.
        assigned_fellows = self.submission.fellows.filter(
            contributor__editorial_assignments__in=self.queryset)
        unassigned_fellows = self.submission.fellows.exclude(
            contributor__editorial_assignments__in=self.queryset)

        possible_assignments = [{ORDERING_FIELD_NAME: -1} for fellow in assigned_fellows]
        for fellow in unassigned_fellows:
            possible_assignments.append({
                'submission': self.submission, 'to': fellow.contributor, ORDERING_FIELD_NAME: -1})
        self.initial = possible_assignments
        self.extra += len(unassigned_fellows)

    def add_fields(self, form, index):
        """Force hidden input for ORDER field."""
        super().add_fields(form, index)
        if ORDERING_FIELD_NAME in form.fields:
            form.fields[ORDERING_FIELD_NAME].widget = forms.HiddenInput()

    def get_form_kwargs(self, index):
        """Add submission to form arguments."""
        kwargs = super().get_form_kwargs(index)
        kwargs['submission'] = self.submission
        return kwargs

    def save(self, commit=True):
        """Save each form and order EditorialAssignments."""
        objects = super().save(commit=False)
        objects = []

        count = 0
        for form in self.ordered_forms:
            ed_assignment = form.save()
            if ed_assignment.id is None:
                continue
            count += 1
            EditorialAssignment.objects.filter(id=ed_assignment.id).update(invitation_order=count)
            objects.append(ed_assignment)
        return objects


PreassignEditorsFormSet = forms.modelformset_factory(
    EditorialAssignment, can_order=True, extra=0,
    formset=BasePreassignEditorsFormSet, form=PreassignEditorsForm)


class SubmissionReassignmentForm(forms.ModelForm):
    """Process reassignment of EIC for Submission."""
    new_editor = forms.ModelChoiceField(queryset=Contributor.objects.none(), required=True)

    class Meta:
        model = Submission
        fields = ()

    def __init__(self, *args, **kwargs):
        """Add related submission as argument."""
        self.submission = kwargs.pop('submission')
        super().__init__(*args, **kwargs)

        self.fields['new_editor'].queryset = Contributor.objects.filter(
            fellowships__in=self.submission.fellows.all()).exclude(
            id=self.submission.editor_in_charge.id)

    def save(self):
        """Update old/create new Assignment and send mails."""
        old_editor = self.submission.editor_in_charge
        old_assignment = self.submission.editorial_assignments.ongoing().filter(
            to=old_editor).first()
        if old_assignment:
            EditorialAssignment.objects.filter(id=old_assignment.id).update(status=STATUS_REPLACED)

        # Update Submission and update/create Editorial Assignments
        now = timezone.now()
        assignment = EditorialAssignment.objects.create(
            submission=self.submission,
            to=self.cleaned_data['new_editor'],
            status=STATUS_ACCEPTED,
            date_invited=now,
            date_answered=now,
        )
        self.submission.editor_in_charge = self.cleaned_data['new_editor']
        self.submission.save()

        # Email old and new editor
        if old_assignment:
            mail_sender = DirectMailUtil(
                mail_code='fellows/email_fellow_replaced_by_other',
                assignment=old_assignment)
            mail_sender.send()

        mail_sender = DirectMailUtil(
            mail_code='fellows/email_fellow_assigned_submission',
            assignment=assignment)
        mail_sender.send()


class SubmissionPrescreeningForm(forms.ModelForm):
    """Processing decision for pre-screening of Submission."""

    PASS, FAIL = 'pass', 'fail'
    CHOICES = (
        (PASS, 'Pass pre-screening. Proceed to the Pool.'),
        (FAIL, 'Fail pre-screening.'))
    decision = forms.ChoiceField(widget=forms.RadioSelect, choices=CHOICES, required=False)

    message_for_authors = forms.CharField(required=False, widget=forms.Textarea({
        'placeholder': 'Message for authors'}))
    remark_for_pool = forms.CharField(required=False, widget=forms.Textarea({
        'placeholder': 'Remark for the pool'}))

    class Meta:
        model = Submission
        fields = ()

    def __init__(self, *args, **kwargs):
        """Add related submission as argument."""
        self.submission = kwargs.pop('submission')
        self.current_user = kwargs.pop('current_user')
        super().__init__(*args, **kwargs)

    def clean(self):
        """Check if Submission has right status."""
        data = super().clean()
        if self.instance.status != STATUS_INCOMING:
            self.add_error(None, 'This Submission is currently not in pre-screening.')

        if data['decision'] == self.PASS:
            if not self.instance.fellows.exists():
                self.add_error(None, 'Please add at least one fellow to the pool first.')
            if not self.instance.editorial_assignments.exists():
                self.add_error(None, 'Please complete the pre-assignments form first.')
        return data

    @transaction.atomic
    def save(self):
        """Update Submission status."""
        if self.cleaned_data['decision'] == self.PASS:
            Submission.objects.filter(id=self.instance.id).update(
                status=STATUS_UNASSIGNED, visible_pool=True, visible_public=False)
            self.instance.add_general_event('Submission passed pre-screening.')
        elif self.cleaned_data['decision'] == self.FAIL:
            Submission.objects.filter(id=self.instance.id).update(
                status=STATUS_FAILED_PRESCREENING, visible_pool=False, visible_public=False)
            self.instance.add_general_event('Submission failed pre-screening.')

        if self.cleaned_data['remark_for_pool']:
            Remark.objects.create(
                submission=self.instance,
                contributor=self.current_user.contributor,
                remark=self.cleaned_data['remark_for_pool'])
        if self.cleaned_data['message_for_authors']:
            pass


class WithdrawSubmissionForm(forms.ModelForm):
    """
    A submitting author has the right to withdraw the manuscript.
    """

    confirm = forms.ChoiceField(
        widget=forms.RadioSelect, choices=((True, 'Confirm'), (False, 'Abort')), label='')

    def __init__(self, *args, **kwargs):
        """Add related submission as argument."""
        self.submission = kwargs.pop('submission')
        super().__init__(*args, **kwargs)

    def is_confirmed(self):
        return self.cleaned_data.get('confirm') in (True, 'True')

    def save(self):
        if self.is_confirmed():
            # Update submission (current + any previous versions)
            Submission.objects.filter(id=self.instance.id).update(
                visible_public=False, visible_pool=False,
                open_for_commenting=False, open_for_reporting=False,
                status=STATUS_WITHDRAWN, latest_activity=timezone.now())
            self.instance.get_other_versions().update(visible_public=False)

            # Update all assignments
            EditorialAssignment.objects.filter(submission=self.instance).need_response().update(
                status=STATUS_DEPRECATED)
            EditorialAssignment.objects.filter(submission=self.instance).accepted().update(
                status=STATUS_COMPLETED)

            # Deprecate any outstanding recommendations
            EICRecommendation.objects.filter(submission=self.instance).active().update(
                status=DEPRECATED)
            self.instance.refresh_from_db()
        return self.instance

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

class InviteEditorialAssignmentForm(forms.ModelForm):
    """Invite new Fellow; create EditorialAssignment for Submission."""

    class Meta:
        model = EditorialAssignment
        fields = ('to',)
        labels = {
            'to': 'Fellow',
        }

    def __init__(self, *args, **kwargs):
        """Add related submission as argument."""
        self.submission = kwargs.pop('submission')
        super().__init__(*args, **kwargs)
        self.fields['to'].queryset = Contributor.objects.available().filter(
            fellowships__pool=self.submission).distinct().order_by('user__last_name')

    def save(self, commit=True):
        self.instance.submission = self.submission
        return super().save(commit)


class EditorialAssignmentForm(forms.ModelForm):
    """Create and/or process new EditorialAssignment for Submission."""

    DECISION_CHOICES = (
        ('accept', 'Accept'),
        ('decline', 'Decline'))
    CYCLE_CHOICES = (
        (CYCLE_DEFAULT, 'Normal refereeing cycle'),
        (CYCLE_DIRECT_REC, 'Directly formulate Editorial Recommendation for rejection'))

    decision = forms.ChoiceField(
        widget=forms.RadioSelect, choices=DECISION_CHOICES,
        label="Are you willing to take charge of this Submission?")
    refereeing_cycle = forms.ChoiceField(
        widget=forms.RadioSelect, choices=CYCLE_CHOICES, initial=CYCLE_DEFAULT)
    refusal_reason = forms.ChoiceField(
        choices=ASSIGNMENT_REFUSAL_REASONS)

    class Meta:
        model = EditorialAssignment
        fields = ()  # Don't use the default fields options because of the ordering of fields.

    def __init__(self, *args, **kwargs):
        """Add related submission as argument."""
        self.submission = kwargs.pop('submission')
        self.request = kwargs.pop('request')
        super().__init__(*args, **kwargs)
        if not self.instance.id:
            del self.fields['decision']
            del self.fields['refusal_reason']

    def has_accepted_invite(self):
        """Check if invite is accepted or if voluntered to become EIC."""
        return 'decision' not in self.cleaned_data or self.cleaned_data['decision'] == 'accept'

    def is_normal_cycle(self):
        """Check if normal refereeing cycle is chosen."""
        return self.cleaned_data['refereeing_cycle'] == CYCLE_DEFAULT

    def save(self, commit=True):
        """Save Submission to EditorialAssignment."""
        self.instance.submission = self.submission
        self.instance.date_answered = timezone.now()
        self.instance.to = self.request.user.contributor
        assignment = super().save()  # Save already, in case it's a new recommendation.

        if self.has_accepted_invite():
            # Update related Submission.
            if self.is_normal_cycle():
                # Default Refereeing process
                deadline = timezone.now() + self.instance.submission.submitted_to.refereeing_period

                # Update related Submission.
                Submission.objects.filter(id=self.submission.id).update(
                    refereeing_cycle=CYCLE_DEFAULT,
                    status=STATUS_EIC_ASSIGNED,
                    editor_in_charge=self.request.user.contributor,
                    reporting_deadline=deadline,
                    open_for_reporting=True,
                    open_for_commenting=True,
                    visible_public=True,
                    latest_activity=timezone.now())
            else:
                # Short Refereeing process
                Submission.objects.filter(id=self.submission.id).update(
                    refereeing_cycle=CYCLE_DIRECT_REC,
                    status=STATUS_EIC_ASSIGNED,
                    editor_in_charge=self.request.user.contributor,
                    reporting_deadline=timezone.now(),
                    open_for_reporting=False,
                    open_for_commenting=True,
                    visible_public=False,
                    latest_activity=timezone.now())

            # Implicitly or explicity accept the assignment and deprecate others.
            # assignment.accepted = True  # Deprecated field
            assignment.status = STATUS_ACCEPTED

            # Update all other 'open' invitations
            EditorialAssignment.objects.filter(submission=self.submission).need_response().exclude(
                id=assignment.id).update(status=STATUS_DEPRECATED)
        else:
            # assignment.accepted = False  # Deprecated field
            assignment.status = STATUS_DECLINED
            assignment.refusal_reason = self.cleaned_data['refusal_reason']
        assignment.save()  # Save again to register acceptance
        return assignment


class ConsiderAssignmentForm(forms.Form):
    """Process open EditorialAssignment."""

    accept = forms.ChoiceField(widget=forms.RadioSelect, choices=ASSIGNMENT_BOOL,
                               label="Are you willing to take charge of this Submission?")
    refusal_reason = forms.ChoiceField(choices=ASSIGNMENT_REFUSAL_REASONS, required=False)


class RefereeSearchForm(forms.Form):
    last_name = forms.CharField(widget=forms.TextInput({
        'placeholder': 'Search for a referee in the SciPost Profiles database'}))

    def search(self):
        return Profile.objects.filter(
            last_name__icontains=self.cleaned_data['last_name'])
        # return Profile.objects.annotate(
        #     similarity=TrigramSimilarity('last_name', self.cleaned_data['last_name']),
        # ).filter(similarity__gt=0.3).order_by('-similarity')


class ConsiderRefereeInvitationForm(forms.Form):
    accept = forms.ChoiceField(widget=forms.RadioSelect, choices=ASSIGNMENT_BOOL,
                               label="Are you willing to referee this Submission?")
    refusal_reason = forms.ChoiceField(choices=ASSIGNMENT_REFUSAL_REASONS, required=False)


class SetRefereeingDeadlineForm(forms.Form):
    deadline = forms.DateField(
        required=False, label='', widget=forms.SelectDateWidget(
            years=[timezone.now().year + i for i in range(2)],
            empty_label=("Year", "Month", "Day"),
        ))

    def clean_deadline(self):
        if not self.cleaned_data.get('deadline'):
            self.add_error('deadline', 'Please use a valid date.')
        return self.cleaned_data.get('deadline')


class VotingEligibilityForm(forms.ModelForm):
    """Assign Fellows to vote for EICRecommendation and open its status for voting."""

    eligible_fellows = forms.ModelMultipleChoiceField(
        queryset=Contributor.objects.none(),
        widget=forms.CheckboxSelectMultiple(),
        required=True, label='Eligible for voting')

    class Meta:
        model = EICRecommendation
        fields = ()

    def __init__(self, *args, **kwargs):
        """Get queryset of Contributors eligible for voting."""
        super().__init__(*args, **kwargs)
        secondary_areas = self.instance.submission.secondary_areas
        if not secondary_areas:
            secondary_areas = []

        self.fields['eligible_fellows'].queryset = Contributor.objects.filter(
            fellowships__pool=self.instance.submission).filter(
                Q(EIC=self.instance.submission) |
                Q(expertises__contains=[self.instance.submission.subject_area]) |
                Q(expertises__contains=secondary_areas)).order_by(
                    'user__last_name').distinct()

    def save(self, commit=True):
        """Update EICRecommendation status and save its voters."""
        self.instance.eligible_to_vote = self.cleaned_data['eligible_fellows']
        self.instance.status = PUT_TO_VOTING

        if commit:
            self.instance.save()
            self.instance.submission.touch()
            self.instance.voted_for.add(self.instance.submission.editor_in_charge)
        return self.instance

    def get_eligible_fellows(self):
        return self.fields['eligible_fellows'].queryset


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

class ReportPDFForm(forms.ModelForm):
    class Meta:
        model = Report
        fields = ['pdf_report']


class ReportForm(forms.ModelForm):
    """Write Report form."""

    report_type = REPORT_NORMAL

    class Meta:
        model = Report
        fields = ['qualification', 'strengths', 'weaknesses', 'report', 'requested_changes',
                  'validity', 'significance', 'originality', 'clarity', 'formatting', 'grammar',
                  'recommendation', 'remarks_for_editors', 'anonymous', 'file_attachment']

    def __init__(self, *args, **kwargs):
        if kwargs.get('instance'):
            if kwargs['instance'].is_followup_report:
                # Prefill data from latest report in the series
                latest_report = kwargs['instance'].latest_report_from_thread()
                kwargs.update({
                    'initial': {
                        'qualification': latest_report.qualification,
                        'anonymous': latest_report.anonymous
                    }
                })

        self.submission = kwargs.pop('submission')

        super().__init__(*args, **kwargs)
        self.fields['strengths'].widget.attrs.update({
            'placeholder': ('Give a point-by-point '
                            '(numbered 1-, 2-, ...) list of the paper\'s strengths'),
            'rows': 10,
            'cols': 100
        })
        self.fields['weaknesses'].widget.attrs.update({
            'placeholder': ('Give a point-by-point '
                            '(numbered 1-, 2-, ...) list of the paper\'s weaknesses'),
            'rows': 10,
            'cols': 100
        })
        self.fields['report'].widget.attrs.update({'placeholder': 'Your general remarks',
                                                   'rows': 10, 'cols': 100})
        self.fields['requested_changes'].widget.attrs.update({
            'placeholder': 'Give a numbered (1-, 2-, ...) list of specifically requested changes',
            'cols': 100
        })

        # Required fields on submission; optional on save as draft
        if 'save_submit' in self.data:
            required_fields = ['report', 'recommendation', 'qualification']
        else:
            required_fields = []
        required_fields_label = ['report', 'recommendation', 'qualification']

        # If the Report is not a followup: Explicitly assign more fields as being required!
        if not self.instance.is_followup_report and self.submission.submitted_to.name != SCIPOST_JOURNAL_PHYSICS_PROC:
            required_fields_label += [
                'strengths',
                'weaknesses',
                'requested_changes',
                'validity',
                'significance',
                'originality',
                'clarity',
                'formatting',
                'grammar']
            required_fields += [
                'strengths',
                'weaknesses',
                'requested_changes',
                'validity',
                'significance',
                'originality',
                'clarity',
                'formatting',
                'grammar']

        for field in required_fields:
            self.fields[field].required = True

        # Let user know the field is required!
        for field in required_fields_label:
            self.fields[field].label += ' *'

        if self.submission.eicrecommendations.active().exists():
            # An active EICRecommendation is already formulated. This Report will be flagged.
            self.report_type = REPORT_POST_EDREC

    def save(self):
        """
        Update meta data if ModelForm is submitted (non-draft).
        Possibly overwrite the default status if user asks for saving as draft.
        """
        report = super().save(commit=False)
        report.report_type = self.report_type

        report.submission = self.submission
        report.date_submitted = timezone.now()

        # Save with right status asked by user
        if 'save_draft' in self.data:
            report.status = STATUS_DRAFT
        elif 'save_submit' in self.data:
            report.status = STATUS_UNVETTED

            # Update invitation and report meta data if exist
            updated_invitations = self.submission.referee_invitations.filter(
                referee=report.author).update(fulfilled=True)
            if updated_invitations > 0:
                report.invited = True

            # Check if report author if the report is being flagged on the submission
            if self.submission.referees_flagged:
                if report.author.user.last_name in self.submission.referees_flagged:
                    report.flagged = True
        # r = report.recommendation
        # t = report.qualification
        report.save()
        return report


class VetReportForm(forms.Form):
    action_option = forms.ChoiceField(widget=forms.RadioSelect,
                                      choices=REPORT_ACTION_CHOICES,
                                      required=True, label='Action')
    refusal_reason = forms.ChoiceField(choices=REPORT_REFUSAL_CHOICES, required=False)
    email_response_field = forms.CharField(widget=forms.Textarea(),
                                           label='Justification (optional)', required=False)

    def __init__(self, *args, **kwargs):
        self.report = kwargs.pop('report', None)
        super().__init__(*args, **kwargs)
        self.fields['email_response_field'].widget.attrs.update({
            'placeholder': ('Optional: give a textual justification '
                            '(will be included in the email to the Report\'s author)'),
            'rows': 5
        })

    def clean_refusal_reason(self):
        """Require a refusal reason if report is rejected."""
        reason = self.cleaned_data['refusal_reason']
        if self.cleaned_data['action_option'] == REPORT_ACTION_REFUSE:
            if not reason:
                self.add_error('refusal_reason', 'A reason must be given to refuse a report.')
        return reason

    def process_vetting(self, current_contributor):
        """Set the right report status and update submission fields if needed."""
        report = self.cleaned_data['report']
        if self.cleaned_data['action_option'] == REPORT_ACTION_ACCEPT:
            # Accept the report as is
            Report.objects.filter(id=report.id).update(
                status=STATUS_VETTED,
                vetted_by=current_contributor,
            )
            report.submission.touch()
        elif self.cleaned_data['action_option'] == REPORT_ACTION_REFUSE:
            # The report is rejected
            Report.objects.filter(id=report.id).update(
                status=self.cleaned_data['refusal_reason'],
            )
        else:
            raise exceptions.InvalidReportVettingValue(self.cleaned_data['action_option'])
        report.refresh_from_db()
        return report


###################
# Communications #
###################

class EditorialCommunicationForm(forms.ModelForm):
    class Meta:
        model = EditorialCommunication
        fields = ('text',)
        widgets = {
            'text': forms.Textarea(attrs={
                'rows': 5,
                'placeholder': 'Write your message in this box.'
            }),
        }


######################
# EIC Recommendation #
######################

class EICRecommendationForm(forms.ModelForm):
    """Formulate an EICRecommendation."""

    DAYS_TO_VOTE = 7
    assignment = None
    earlier_recommendations = []

    class Meta:
        model = EICRecommendation
        fields = [
            'recommendation',
            'remarks_for_authors',
            'requested_changes',
            'remarks_for_editorial_college'
        ]
        widgets = {
            'remarks_for_authors': forms.Textarea({
                'placeholder': 'Your general remarks for the authors',
                'rows': 10,
            }),
            'requested_changes': forms.Textarea({
                'placeholder': ('If you request revisions, give a numbered (1-, 2-, ...)'
                                ' list of specifically requested changes'),
            }),
            'remarks_for_editorial_college': forms.Textarea({
                'placeholder': ('If you recommend to accept or refuse, the Editorial College '
                                'will vote; write any relevant remarks for the EC here.'),
            }),
        }

    def __init__(self, *args, **kwargs):
        """Accept two additional kwargs.

        -- submission: The Submission to formulate an EICRecommendation for.
        -- reformulate (bool): Reformulate the currently available EICRecommendations.
        """
        self.submission = kwargs.pop('submission')
        self.reformulate = kwargs.pop('reformulate', False)
        self.load_earlier_recommendations()

        if self.reformulate:
            latest_recommendation = self.earlier_recommendations.first()
            if latest_recommendation:
                kwargs['initial'] = {
                    'recommendation': latest_recommendation.recommendation,
                    'remarks_for_authors': latest_recommendation.remarks_for_authors,
                    'requested_changes': latest_recommendation.requested_changes,
                    'remarks_for_editorial_college':
                        latest_recommendation.remarks_for_editorial_college,
                }

        super().__init__(*args, **kwargs)
        self.load_assignment()

    def save(self):
        recommendation = super().save(commit=False)
        recommendation.submission = self.submission
        recommendation.voting_deadline += datetime.timedelta(days=self.DAYS_TO_VOTE)  # Test this
        recommendation.version = len(self.earlier_recommendations) + 1

        if self.reformulate:
            event_text = 'The Editorial Recommendation has been reformulated: {}.'
        else:
            event_text = 'An Editorial Recommendation has been formulated: {}.'

        if recommendation.recommendation in [REPORT_MINOR_REV, REPORT_MAJOR_REV]:
            # Minor/Major revision: return to Author; ask to resubmit
            recommendation.status = DECISION_FIXED
            Submission.objects.filter(id=self.submission.id).update(
                open_for_reporting=False,
                open_for_commenting=False,
                reporting_deadline=timezone.now())

            if self.assignment:
                # The EIC has fulfilled this editorial assignment.
                self.assignment.status = STATUS_COMPLETED
                self.assignment.save()

            # Add SubmissionEvents for both Author and EIC
            self.submission.add_general_event(event_text.format(
                recommendation.get_recommendation_display()))
        else:
            # Add SubmissionEvent for EIC only
            self.submission.add_event_for_eic(event_text.format(
                recommendation.get_recommendation_display()))

        if self.earlier_recommendations:
            self.earlier_recommendations.update(active=False, status=DEPRECATED)

            # All reports already submitted are now formulated *after* eic rec formulation
            Report.objects.filter(
                submission__eicrecommendations__in=self.earlier_recommendations).update(
                    report_type=REPORT_NORMAL)

        recommendation.save()

        return recommendation

    def revision_requested(self):
        return self.instance.recommendation in [REPORT_MINOR_REV, REPORT_MAJOR_REV]

    def has_assignment(self):
        return self.assignment is not None

    def load_assignment(self):
        # Find EditorialAssignment for Submission
        try:
            self.assignment = self.submission.editorial_assignments.accepted().get(
                to=self.submission.editor_in_charge)
            return True
        except EditorialAssignment.DoesNotExist:
            return False

    def load_earlier_recommendations(self):
        """Load and save EICRecommendations related to Submission of the instance."""
        self.earlier_recommendations = self.submission.eicrecommendations.all()


###############
# Vote form #
###############

class RecommendationVoteForm(forms.Form):
    """Cast vote on EICRecommendation form."""

    vote = forms.ChoiceField(
        widget=forms.RadioSelect, choices=[
            ('agree', 'Agree'), ('disagree', 'Disagree'), ('abstain', 'Abstain')])
    remark = forms.CharField(widget=forms.Textarea(attrs={
        'rows': 3,
        'cols': 30,
        'placeholder': 'Your remark (optional)'
    }), label='', required=False)


class SubmissionCycleChoiceForm(forms.ModelForm):
    """Make a decision on the Submission's cycle and make publicly available."""

    referees_reinvite = forms.ModelMultipleChoiceField(
        queryset=RefereeInvitation.objects.none(),
        widget=forms.CheckboxSelectMultiple({'checked': 'checked'}),
        required=False, label='Reinvite referees')

    class Meta:
        model = Submission
        fields = ('refereeing_cycle',)
        widgets = {'refereeing_cycle': forms.RadioSelect}

    def __init__(self, *args, **kwargs):
        """Update choices and queryset."""
        super().__init__(*args, **kwargs)
        self.fields['refereeing_cycle'].choices = SUBMISSION_CYCLE_CHOICES
        other_submissions = self.instance.other_versions.all()
        if other_submissions:
            self.fields['referees_reinvite'].queryset = RefereeInvitation.objects.filter(
                submission__in=other_submissions).distinct()

    def save(self):
        """Make Submission publicly available after decision."""
        self.instance.visible_public = True
        return super().save()


class iThenticateReportForm(forms.ModelForm):
    class Meta:
        model = iThenticateReport
        fields = []

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

        if kwargs.get('files', {}).get('file'):
            # Add file field if file data is coming in!
            self.fields['file'] = forms.FileField()

    def clean(self):
        cleaned_data = super().clean()
        doc_id = self.instance.doc_id
        if not doc_id and not self.fields.get('file'):
            try:
                cleaned_data['document'] = helpers.retrieve_pdf_from_arxiv(
                    self.submission.preprint.identifier_w_vn_nr)
            except exceptions.ArxivPDFNotFound:
                self.add_error(
                    None, 'The pdf could not be found at arXiv. Please upload the pdf manually.')
                self.fields['file'] = forms.FileField()
        elif not doc_id and cleaned_data.get('file'):
            cleaned_data['document'] = cleaned_data['file'].read()
        elif doc_id:
            self.document_id = doc_id

        # Login client to append login-check to form
        self.client = self.get_client()

        if not self.client:
            return None

        # Document (id) is found
        if cleaned_data.get('document'):
            self.document = cleaned_data['document']
            try:
                self.response = self.call_ithenticate()
            except AttributeError:
                if not self.fields.get('file'):
                    # The document is invalid.
                    self.add_error(None, ('A valid pdf could not be found at arXiv.'
                                          ' Please upload the pdf manually.'))
                else:
                    self.add_error(None, ('The uploaded file is not valid.'
                                          ' Please upload a valid pdf.'))
                self.fields['file'] = forms.FileField()
        elif hasattr(self, 'document_id'):
            self.response = self.call_ithenticate()

        if hasattr(self, 'response') and self.response:
            return cleaned_data

        # Don't return anything as someone submitted invalid data for the form at this point!
        return None

    def save(self, *args, **kwargs):
        data = self.response

        report, created = iThenticateReport.objects.get_or_create(doc_id=data['id'])

        if not created:
            try:
                iThenticateReport.objects.filter(doc_id=data['id']).update(
                    uploaded_time=data['uploaded_time'],
                    processed_time=data['processed_time'],
                    percent_match=data['percent_match'],
                    part_id=data.get('parts', [{}])[0].get('id')
                )
            except KeyError:
                pass
        else:
            report.save()
            Submission.objects.filter(id=self.submission.id).update(plagiarism_report=report)
        return report

    def call_ithenticate(self):
        if hasattr(self, 'document_id'):
            # Update iThenticate status
            return self.update_status()
        elif hasattr(self, 'document'):
            # Upload iThenticate document first time
            return self.upload_document()

    def get_client(self):
        client = iThenticate.API.Client(settings.ITHENTICATE_USERNAME,
                                        settings.ITHENTICATE_PASSWORD)
        if client.login():
            return client
        self.add_error(None, "Failed to login to iThenticate.")
        return None

    def update_status(self):
        client = self.client
        response = client.documents.get(self.document_id)
        if response['status'] == 200:
            return response.get('data')[0].get('documents')[0]
        self.add_error(None, "Updating failed. iThenticate didn't return valid data [1]")

        for msg in client.messages:
            self.add_error(None, msg)
        return None

    def upload_document(self):
        from .plagiarism import iThenticate
        plagiarism = iThenticate()
        data = plagiarism.upload_submission(self.document, self.submission)

        # Give feedback to the user
        if not data:
            self.add_error(None, "Updating failed. iThenticate didn't return valid data [3]")
            for msg in plagiarism.get_messages():
                self.add_error(None, msg)
            return None
        return data


class FixCollegeDecisionForm(forms.ModelForm):
    """Fix EICRecommendation decision."""

    FIX, DEPRECATE = 'fix', 'deprecate'
    action = forms.ChoiceField(choices=((FIX, FIX), (DEPRECATE, DEPRECATE)))

    class Meta:
        model = EICRecommendation
        fields = ()

    def __init__(self, *args, **kwargs):
        """Accept request as argument."""
        self.submission = kwargs.pop('submission', None)
        self.request = kwargs.pop('request', None)
        return super().__init__(*args, **kwargs)

    def clean(self):
        """Check if EICRecommendation has the right decision."""
        data = super().clean()
        if self.instance.status == DECISION_FIXED:
            self.add_error(None, 'This EICRecommendation is already fixed.')
        elif self.instance.status == DEPRECATED:
            self.add_error(None, 'This EICRecommendation is deprecated.')
        return data

    def is_fixed(self):
        """Check if decision is fixed."""
        return self.cleaned_data['action'] == self.FIX

    def fix_decision(self, recommendation):
        """Fix decision of EICRecommendation."""
        EICRecommendation.objects.filter(id=recommendation.id).update(
            status=DECISION_FIXED)
        submission = recommendation.submission
        if recommendation.recommendation in [REPORT_PUBLISH_1, REPORT_PUBLISH_2, REPORT_PUBLISH_3]:
            # Publish as Tier I, II or III
            Submission.objects.filter(id=submission.id).update(
                visible_public=True, status=STATUS_ACCEPTED, acceptance_date=datetime.date.today(),
                latest_activity=timezone.now())

            # Start a new ProductionStream
            get_or_create_production_stream(submission)

            if self.request:
                # Add SubmissionEvent for authors
                notify_manuscript_accepted(self.request.user, submission, False)
        elif recommendation.recommendation == REPORT_REJECT:
            # Decision: Rejection. Auto hide from public and Pool.
            Submission.objects.filter(id=submission.id).update(
                visible_public=False, visible_pool=False,
                status=STATUS_REJECTED, latest_activity=timezone.now())
            submission.get_other_versions().update(visible_public=False)

        # Force-close the refereeing round for new referees.
        Submission.objects.filter(id=submission.id).update(
            open_for_reporting=False,
            open_for_commenting=False)

        # Update Editorial Assignment statuses.
        EditorialAssignment.objects.filter(
            submission=submission, to=submission.editor_in_charge).update(status=STATUS_COMPLETED)

        # Add SubmissionEvent for authors
        submission.add_event_for_author(
            'The Editorial Recommendation has been formulated: {0}.'.format(
                recommendation.get_recommendation_display()))
        submission.add_event_for_eic(
            'The Editorial Recommendation has been fixed: {0}.'.format(
                recommendation.get_recommendation_display()))
        return recommendation

    def deprecate_decision(self, recommendation):
        """Deprecate decision of EICRecommendation."""
        EICRecommendation.objects.filter(id=recommendation.id).update(
            status=DEPRECATED, active=False)
        recommendation.submission.add_event_for_eic(
            'The Editorial Recommendation (version {version}) has been deprecated: {decision}.'.format(
                version=recommendation.version,
                decision=recommendation.get_recommendation_display()))

        return recommendation

    def save(self):
        """Update EICRecommendation and related Submission."""
        if self.is_fixed():
            return self.fix_decision(self.instance)
        elif self.cleaned_data['action'] == self.DEPRECATE:
            return self.deprecate_decision(self.instance)
        else:
            raise ValueError('The decision given is invalid')
        return self.instance