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


from django.db import models
from django.contrib.contenttypes.fields import GenericRelation
from django.urls import reverse
from django.contrib.auth.models import User
from profiles.models import Profile
from django.utils import timezone
from django.utils.functional import cached_property
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.db.models import Value
from django.db.models.functions import Concat
from django.conf import settings

from .constants import (
    PRODUCTION_STREAM_STATUS,
    PRODUCTION_STREAM_INITIATED,
    PRODUCTION_EVENTS,
    EVENT_MESSAGE,
    EVENT_HOUR_REGISTRATION,
    PRODUCTION_STREAM_COMPLETED,
    PROOFS_STATUSES,
    PROOFS_UPLOADED,
    PROOFS_REPO_STATUSES,
    PROOFS_REPO_UNINITIALIZED,
)
from .managers import (
    ProductionStreamQuerySet,
    ProductionEventManager,
    ProofsQuerySet,
    ProductionUserQuerySet,
)
from .utils import proofs_id_to_slug

from finances.models import WorkLog
from scipost.storage import SecureFileStorage


class ProductionUser(models.Model):
    """
    Production Officers will have a ProductionUser object related to their account
    to relate all production related actions to.
    """

    user = models.OneToOneField(
        User,
        on_delete=models.PROTECT,
        unique=True,
        related_name="production_user",
        null=True,
    )
    name = models.CharField(max_length=128, blank=True)

    objects = ProductionUserQuerySet.as_manager()

    def __str__(self):
        if self.user:
            return "%s, %s" % (self.user.last_name, self.user.first_name)
        return "%s (deactivated)" % self.name


class ProductionStream(models.Model):
    submission = models.OneToOneField(
        "submissions.Submission",
        on_delete=models.CASCADE,
        related_name="production_stream",
    )
    opened = models.DateTimeField(auto_now_add=True)
    closed = models.DateTimeField(default=timezone.now)
    status = models.CharField(
        max_length=32,
        choices=PRODUCTION_STREAM_STATUS,
        default=PRODUCTION_STREAM_INITIATED,
    )

    officer = models.ForeignKey(
        "production.ProductionUser",
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="streams",
    )
    supervisor = models.ForeignKey(
        "production.ProductionUser",
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="supervised_streams",
    )
    invitations_officer = models.ForeignKey(
        "production.ProductionUser",
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
        related_name="invitations_officer_streams",
    )

    work_logs = GenericRelation(WorkLog, related_query_name="streams")

    objects = ProductionStreamQuerySet.as_manager()

    class Meta:
        permissions = (
            ("can_work_for_stream", "Can work for stream"),
            ("can_perform_supervisory_actions", "Can perform supervisory actions"),
        )

    def __str__(self):
        return "{arxiv}, {title}".format(
            arxiv=self.submission.preprint.identifier_w_vn_nr,
            title=self.submission.title,
        )

    def get_absolute_url(self):
        return reverse("production:stream", args=(self.id,))

    @cached_property
    def total_duration(self):
        totdur = self.work_logs.aggregate(models.Sum("duration"))
        return totdur["duration__sum"]

    @cached_property
    def completed(self):
        return self.status == PRODUCTION_STREAM_COMPLETED

    @property
    def latest_activity(self):
        if self.events.last():
            return self.events.last().noted_on
        return self.closed or self.opened


class ProductionEvent(models.Model):
    stream = models.ForeignKey(ProductionStream, on_delete=models.CASCADE, related_name="events")
    event = models.CharField(max_length=64, choices=PRODUCTION_EVENTS, default=EVENT_MESSAGE)
    comments = models.TextField(blank=True, null=True)
    noted_on = models.DateTimeField(default=timezone.now)
    noted_by = models.ForeignKey("production.ProductionUser", on_delete=models.CASCADE, related_name="events")
    noted_to = models.ForeignKey(
        "production.ProductionUser",
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        related_name="received_events",
    )
    duration = models.DurationField(blank=True, null=True)

    objects = ProductionEventManager()

    class Meta:
        ordering = ["noted_on"]

    def __str__(self):
        return "%s: %s" % (self.stream, self.get_event_display())

    def get_absolute_url(self):
        return self.stream.get_absolute_url()

    @cached_property
    def editable(self):
        return self.event in [EVENT_MESSAGE, EVENT_HOUR_REGISTRATION] and not self.stream.completed


def production_event_upload_location(instance, filename):
    submission = instance.production_event.stream.submission
    return "UPLOADS/PRODSTREAMS/{year}/{thread_hash_head}/{filename}".format(
        year=submission.submission_date.year,
        thread_hash_head=str(submission.thread_hash).partition("-")[0],
        filename=filename,
    )


class ProductionEventAttachment(models.Model):
    """
    An ProductionEventAttachment is in general used by authors to reply to a Proofs version
    with their version of the Proofs with comments.
    """

    production_event = models.ForeignKey(
        "production.ProductionEvent",
        on_delete=models.CASCADE,
        related_name="attachments",
    )
    attachment = models.FileField(upload_to=production_event_upload_location, storage=SecureFileStorage())

    def get_absolute_url(self):
        return reverse(
            "production:production_event_attachment_pdf",
            args=(
                self.production_event.stream.id,
                self.id,
            ),
        )


def proofs_upload_location(instance, filename):
    submission = instance.stream.submission
    return "UPLOADS/PROOFS/{year}/{thread_hash_head}/{filename}".format(
        year=submission.submission_date.year,
        thread_hash_head=str(submission.thread_hash).partition("-")[0],
        filename=filename,
    )


class Proofs(models.Model):
    """
    Proofs are directly related to a ProductionStream and Submission in SciPost.
    """

    attachment = models.FileField(upload_to=proofs_upload_location, storage=SecureFileStorage())
    version = models.PositiveSmallIntegerField(default=0)
    stream = models.ForeignKey("production.ProductionStream", on_delete=models.CASCADE, related_name="proofs")
    uploaded_by = models.ForeignKey("production.ProductionUser", on_delete=models.CASCADE, related_name="+")
    created = models.DateTimeField(auto_now_add=True)
    status = models.CharField(max_length=16, choices=PROOFS_STATUSES, default=PROOFS_UPLOADED)
    accessible_for_authors = models.BooleanField(default=False)

    objects = ProofsQuerySet.as_manager()

    class Meta:
        ordering = ["stream", "version"]
        verbose_name_plural = "Proofs"

    def get_absolute_url(self):
        return reverse("production:proofs_pdf", kwargs={"slug": self.slug})

    def __str__(self):
        return "Proofs {version} for Stream {stream}".format(version=self.version, stream=self.stream.submission.title)

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

    @property
    def slug(self):
        return proofs_id_to_slug(self.id)


class ProofsRepository(models.Model):
    """
    ProofsRepository is a GitLab repository of Proofs for a Submission.
    """

    stream = models.OneToOneField(
        ProductionStream,
        on_delete=models.CASCADE,
        related_name="proofs_repository",
    )
    status = models.CharField(
        max_length=32,
        choices=PROOFS_REPO_STATUSES,
        default=PROOFS_REPO_UNINITIALIZED,
    )

    @property
    def name(self) -> str:
        """
        Return the name of the repository in the form of "id_lastname".
        """
        # Get the last name of the first author by getting the first author string from the submission
        first_author_str = self.stream.submission.authors_as_list[0]
        first_author_profile = (
            Profile.objects.annotate(
                full_name=Concat("first_name", Value(" "), "last_name")
            )
            .filter(full_name=first_author_str)
            .first()
        )
        if first_author_profile is None:
            first_author_last_name = first_author_str.split(" ")[-1]
        else:
            first_author_last_name = first_author_profile.last_name
            # Keep only the last of the last names
            first_author_last_name = first_author_last_name.split(" ")[-1]

        return "{preprint_id}_{last_name}".format(
            preprint_id=self.stream.submission.preprint.identifier_w_vn_nr,
            last_name=first_author_last_name,
        )

    @property
    def journal_abbrev(self) -> str:
        # The DOI label is used to determine the path of the repository and template
        return self.stream.submission.editorial_decision.for_journal.doi_label

    @property
    def journal_subdivision(self) -> str:
        """
        Return the subdivision of the repository depending on the journal type.
        Regular journals are subdivided per year and month,
        while proceedings are subdivided per year and conference.
        """

        # TODO: Removing the whitespace should be more standardised
        # Refactor: journal and year are common to both cases
        # perhaps it is best to only return the subdivision month/conference
        if proceedings_issue := self.stream.submission.proceedings:
            return "{journal}/{year}/{conference}".format(
                journal=self.journal_abbrev,
                year=self.stream.submission.proceedings.event_end_date.year,
                conference=proceedings_issue.event_suffix.replace(" ", ""),
            )
        else:
            # Get creation date of the stream
            # Warning: The month grouping of streams was done using the tasked date,
            # but should now instead be the creation (opened) date.
            opened_year, opened_month = self.stream.opened.strftime("%Y-%m").split("-")

            return "{journal}/{year}/{month}".format(
                journal=self.journal_abbrev,
                year=opened_year,
                month=opened_month,
            )

    @property
    def git_path(self) -> str:
        return "{ROOT}/Proofs/{journal_subdivision}/{repo_name}".format(
            ROOT=settings.GITLAB_ROOT,
            journal_subdivision=self.journal_subdivision,
            repo_name=self.name,
        )

    @property
    def git_url(self) -> str:
        return "https://{GITLAB_URL}/{git_path}".format(
            GITLAB_URL=settings.GITLAB_URL,
            git_path=self.git_path,
        )

    @property
    def git_ssh_clone_url(self) -> str:
        return "git:{GITLAB_URL}/{git_path}.git".format(
            GITLAB_URL=settings.GITLAB_URL,
            git_path=self.git_path,
        )

    @property
    def template_path(self) -> str:
        """
        Return the path to the template repository.
        """
        if self.stream.submission.proceedings is not None:
            return "{ROOT}/Templates/{journal_subdivision}".format(
                ROOT=settings.GITLAB_ROOT,
                journal_subdivision=self.journal_subdivision,
            )
        else:
            return "{ROOT}/Templates/{journal}".format(
                ROOT=settings.GITLAB_ROOT,
                journal=self.journal_abbrev,
            )

    def __str__(self) -> str:
        return f"Proofs repo for {self.stream}"

    class Meta:
        verbose_name_plural = "proofs repositories"


@receiver(post_save, sender=ProductionStream)
def production_stream_create_proofs_repo(sender, instance, created, **kwargs):
    """
    When a ProductionStream instance is created, a ProofsRepository instance is created
    and linked to it.
    """
    if created:
        ProofsRepository.objects.create(
            stream=instance,
            status=PROOFS_REPO_UNINITIALIZED,
        )


post_save.connect(production_stream_create_proofs_repo, sender=ProductionStream)


@receiver(post_save, sender=ProofsRepository)
def advance_repo_on_gitlab(sender, instance, created, **kwargs):
    """
    When a ProofsRepository instance is created, run the advance_git_repos command on it.
    """
    if created:
        from django.core import management

        management.call_command(
            "advance_git_repos",
            id=instance.stream.submission.preprint.identifier_w_vn_nr,
        )


post_save.connect(advance_repo_on_gitlab, sender=ProofsRepository)