import re

from datetime import datetime

from django import forms
from django.forms import BaseModelFormSet, modelformset_factory
from django.utils import timezone

from .constants import STATUS_DRAFT, PUBLICATION_PREPUBLISHED
from .exceptions import PaperNumberingError
from .models import Issue, Publication, Reference, UnregisteredAuthor, PublicationAuthorsTable

from funders.models import Grant
from mails.utils import DirectMailUtil
from scipost.services import DOICaller
from submissions.models import Submission


class InitiatePublicationForm(forms.Form):
    accepted_submission = forms.ModelChoiceField(queryset=Submission.objects.accepted())
    to_be_issued_in = forms.ModelChoiceField(
        queryset=Issue.objects.filter(until_date__gte=timezone.now()))

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


class ValidatePublicationForm(forms.ModelForm):
    class Meta:
        model = Publication
        exclude = ['authors_claims', 'authors_false_claims',
                   'metadata', 'metadata_xml', 'authors_registered',
                   'authors_unregistered', 'latest_activity']


class UnregisteredAuthorForm(forms.ModelForm):
    class Meta:
        model = UnregisteredAuthor
        fields = ('first_name', 'last_name')


class CitationListBibitemsForm(forms.Form):
    latex_bibitems = forms.CharField(widget=forms.Textarea())

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['latex_bibitems'].widget.attrs.update(
            {'placeholder': 'Paste the .tex bibitems here'})

    def extract_dois(self):
        entries_list = self.cleaned_data['latex_bibitems']
        entries_list = re.sub(r'(?m)^\%.*\n?', '', entries_list)
        entries_list = entries_list.split('\doi{')
        dois = []
        nentries = 1
        for entry in entries_list[1:]:  # drop first bit before first \doi{
            dois.append(
                {'key': 'ref' + str(nentries),
                 'doi': entry.partition('}')[0], }
            )
            nentries += 1
        return dois


class FundingInfoForm(forms.ModelForm):
    funding_statement = forms.CharField(widget=forms.Textarea({
        'placeholder': 'Paste the funding info statement here'}))

    class Meta:
        model = Publication
        fields = ()

    def save(self, *args, **kwargs):
        self.instance.metadata['funding_statement'] = self.cleaned_data['funding_statement']
        return super().save(*args, **kwargs)


class CreateMetadataXMLForm(forms.ModelForm):
    class Meta:
        model = Publication
        fields = ['metadata_xml']


class CreateMetadataDOAJForm(forms.ModelForm):
    class Meta:
        model = Publication
        fields = ()

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

    def save(self, *args, **kwargs):
        self.instance.metadata_DOAJ = self.generate(self.instance)
        return super().save(*args, **kwargs)

    def generate(self, publication):
        md = {
            'bibjson': {
                'author': [{'name': publication.author_list}],
                'title': publication.title,
                'abstract': publication.abstract,
                'year': publication.publication_date.strftime('%Y'),
                'month': publication.publication_date.strftime('%m'),
                'start_page': publication.get_paper_nr(),
                'identifier': [
                    {
                        'type': 'eissn',
                        'id': str(publication.in_issue.in_volume.in_journal.issn)
                    },
                    {
                        'type': 'doi',
                        'id': publication.doi_string
                    }
                ],
                'link': [
                    {
                        'url': self.request.build_absolute_uri(publication.get_absolute_url()),
                        'type': 'fulltext',
                    }
                ],
                'journal': {
                    'publisher': 'SciPost',
                    'volume': str(publication.in_issue.in_volume.number),
                    'number': str(publication.in_issue.number),
                    'identifier': [{
                        'type': 'eissn',
                        'id': str(publication.in_issue.in_volume.in_journal.issn)
                    }],
                    'license': [
                        {
                            'url': self.request.build_absolute_uri(
                                publication.in_issue.in_volume.in_journal.get_absolute_url()),
                            'open_access': 'true',
                            'type': publication.get_cc_license_display(),
                            'title': publication.get_cc_license_display(),
                        }
                    ],
                    'language': ['EN'],
                    'title': publication.in_issue.in_volume.in_journal.get_name_display(),
                }
            }
        }
        return md


class BaseReferenceFormSet(BaseModelFormSet):
    """
    BaseReferenceFormSet is used to help fill the Reference list for Publications

    It is required to add the required keyword argument `publication` to this FormSet.
    """
    initial_references = []

    def __init__(self, *args, **kwargs):
        self.publication = kwargs.pop('publication')
        extra = kwargs.pop('extra')
        self.extra = int(extra if extra else '0')
        kwargs['form_kwargs'] = {'publication': self.publication}
        super().__init__(*args, **kwargs)

    def prefill(self):
        citations = self.publication.metadata.get('citation_list', [])

        for cite in citations:
            caller = DOICaller(cite['doi'])

            if caller.is_valid:
                # Authors
                author_list = []
                for author in caller._crossref_data['author'][:3]:
                    try:
                        author_list.append('{}. {}'.format(author['given'][0], author['family']))
                    except KeyError:
                        author_list.append(author['name'])

                if len(author_list) > 2:
                    authors = ', '.join(author_list[:-1])
                    authors += ' and ' + author_list[-1]
                else:
                    authors = ' and '.join(author_list)

                # Citation
                citation = '<em>{}</em> {} <b>{}</b>, {} ({})'.format(
                    caller.data['title'],
                    caller.data['journal'],
                    caller.data['volume'],
                    caller.data['pages'],
                    datetime.strptime(caller.data['pub_date'], '%Y-%m-%d').year)

                self.initial_references.append({
                    'reference_number': cite['key'][3:],
                    'authors': authors,
                    'citation': citation,
                    'identifier': cite['doi'],
                    'link': 'https://doi.org/{}'.format(cite['doi']),
                })
            else:
                self.initial_references.append({
                    'reference_number': cite['key'][3:],
                    'identifier': cite['doi'],
                    'link': 'https://doi.org/{}'.format(cite['doi']),
                })

        # Add prefill information to the form
        if not self.initial_extra:
            self.initial_extra = self.initial_references
        else:
            self.initial_extra.extend(self.initial_references)
        self.extra += len(self.initial_extra)


class ReferenceForm(forms.ModelForm):
    class Meta:
        model = Reference
        fields = [
            'reference_number',
            'authors',
            'citation',
            'identifier',
            'link',
        ]

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

    def save(self, *args, **kwargs):
        self.instance.publication = self.publication
        super().save(*args, **kwargs)


ReferenceFormSet = modelformset_factory(Reference, formset=BaseReferenceFormSet,
                                        form=ReferenceForm, can_delete=True)


class DraftPublicationForm(forms.ModelForm):
    """
    This Form is used by the Production Supervisors to create a new Publication object
    and prefill all data. It is only able to create a `draft` version of a Publication object.
    """
    class Meta:
        model = Publication
        fields = [
            'doi_label',
            'pdf_file',
            'in_issue',
            'paper_nr',
            'title',
            'author_list',
            'abstract',
            'discipline',
            'domain',
            'subject_area',
            'secondary_areas',
            'cc_license',
            'BiBTeX_entry',
            'submission_date',
            'acceptance_date',
            'publication_date']

    def __init__(self, data=None, arxiv_identifier_w_vn_nr=None, issue_id=None, *args, **kwargs):
        # Use separate instance to be able to prefill the form without any existing Publication
        self.submission = None
        self.issue = None
        if arxiv_identifier_w_vn_nr:
            self.submission = Submission.objects.accepted().get(
                arxiv_identifier_w_vn_nr=arxiv_identifier_w_vn_nr)
        if issue_id:
            self.issue = Issue.objects.filter(until_date__gte=timezone.now()).get(id=issue_id)
        super().__init__(data, *args, **kwargs)
        if kwargs.get('instance') or self.issue:
            # When updating: fix in_issue, because many fields are directly related to the issue.
            del self.fields['in_issue']
            self.prefill_fields()
        else:
            self.fields['in_issue'].queryset = Issue.objects.filter(until_date__gte=timezone.now())
            self.delete_secondary_fields()


    def delete_secondary_fields(self):
        """
        Delete fields from the self.fields dictionary. Later on, this submitted sparse form can
        be used to prefill these secondary fields.
        """
        del self.fields['doi_label']
        del self.fields['pdf_file']
        del self.fields['paper_nr']
        del self.fields['title']
        del self.fields['author_list']
        del self.fields['abstract']
        del self.fields['discipline']
        del self.fields['domain']
        del self.fields['subject_area']
        del self.fields['secondary_areas']
        del self.fields['cc_license']
        del self.fields['BiBTeX_entry']
        del self.fields['submission_date']
        del self.fields['acceptance_date']
        del self.fields['publication_date']

    def save(self, *args, **kwargs):
        """
        Save the Publication object always as a draft and prefill the Publication with
        related Submission data only when appending the Publication.
        """
        self.instance.status = STATUS_DRAFT
        do_prefill = False
        if not self.instance.id:
            do_prefill = True
            self.instance.accepted_submission = self.submission
            self.instance.in_issue = self.issue
        self.instance = super().save(*args, **kwargs)
        if do_prefill:
            self.first_time_fill()
        return self.instance

    def first_time_fill(self):
        """
        Take over fields from related Submission object. This can only be done after
        the Publication object has been added to the database due to m2m relations.
        """
        # Copy all existing author and non-author relations to Publication
        for submission_author in self.submission.authors.all():
            PublicationAuthorsTable.objects.create(
                publication=self.instance, contributor=submission_author)
        self.instance.authors_claims.add(*self.submission.authors_claims.all())
        self.instance.authors_false_claims.add(*self.submission.authors_false_claims.all())

        # Add Institutions to the publication related to the current authors
        for author in self.instance.authors_registered.all():
            for current_affiliation in author.affiliations.active():
                self.instance.institutions.add(current_affiliation.institution)

    def prefill_fields(self):
        if self.submission:
            self.fields['title'].initial = self.submission.title
            self.fields['author_list'].initial = self.submission.author_list
            self.fields['abstract'].initial = self.submission.abstract
            self.fields['discipline'].initial = self.submission.discipline
            self.fields['domain'].initial = self.submission.domain
            self.fields['subject_area'].initial = self.submission.subject_area
            self.fields['secondary_areas'].initial = self.submission.secondary_areas
            self.fields['submission_date'].initial = self.submission.submission_date
            self.fields['acceptance_date'].initial = self.submission.acceptance_date

        # Fill data that may be derived from the issue data
        issue = self.instance.in_issue if hasattr(self.instance, 'in_issue') else self.issue
        if issue:
            # Determine next available paper number:
            paper_nr = Publication.objects.filter(in_issue__in_volume=issue.in_volume).count()
            paper_nr += paper_nr
            if paper_nr > 999:
                raise PaperNumberingError(paper_nr)
            self.fields['paper_nr'].initial = str(paper_nr)
            doi_label = '{journal}.{vol}.{issue}.{paper}'.format(
                journal=issue.in_volume.in_journal.name,
                vol=issue.in_volume.number,
                issue=issue.number,
                paper=str(paper_nr).rjust(3, '0'))
            self.fields['doi_label'].initial = doi_label

            doi_string = '10.21468/{doi}'.format(doi=doi_label)
            bibtex_entry = (
                '@Article{%s,\n'
                '\ttitle={{%s},\n'
                '\tauthor={%s},\n'
                '\tjournal={%s},\n'
                '\tvolume={%i},\n'
                '\tissue={%i},\n'
                '\tpages={%i},\n'
                '\tyear={%s},\n'
                '\tpublisher={SciPost},\n'
                '\tdoi={%s},\n'
                '\turl={https://scipost.org/%s},\n'
                '}'
            ) % (
                doi_string,
                self.submission.title,
                self.submission.author_list.replace(',', ' and'),
                issue.in_volume.in_journal.get_abbreviation_citation(),
                issue.in_volume.number,
                issue.number,
                paper_nr,
                issue.until_date.strftime('%Y'),
                doi_string,
                doi_string)
            self.fields['BiBTeX_entry'].initial = bibtex_entry
            if not self.instance.BiBTeX_entry:
                self.instance.BiBTeX_entry = bibtex_entry


class DraftPublicationApprovalForm(forms.ModelForm):
    class Meta:
        model = Publication
        fields = ()

    def save(self, commit=True):
        self.instance.status = PUBLICATION_PREPUBLISHED
        if commit:
            self.instance.save()
            mail_sender = DirectMailUtil(mail_code='publication_ready', instance=self.instance)
            mail_sender.send()
        return self.instance


class PublicationGrantsForm(forms.ModelForm):
    grant = forms.ModelChoiceField(queryset=Grant.objects.none())

    class Meta:
        model = Publication
        fields = []

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['grant'].queryset = Grant.objects.exclude(
            id__in=self.instance.grants.values_list('id', flat=True))

    def save(self, commit=True):
        if commit:
            self.instance.grants.add(self.cleaned_data['grant'])
        return self.instance