"""_____________________________________________________________________

:PROJECT: LARAsuite

*lara_django_processes models for process instances and their m2m tables*

:details: lara_django_processes database models.

:authors: - mark doerr <mark.doerr@uni-greifswald.de>
          - marco di sarno <disarno@k3b.de>

.. note:: -
.. todo:: -
________________________________________________________________________
"""
import uuid
from datetime import datetime

from django.conf import settings
from django.db import IntegrityError
from django.db import models
from django.utils import timezone

from lara_django_base.models import ItemStatus
from lara_django_people.models import ItemSubsetAbstr
from .abstracts import ProcessInstanceAbstr
from .procedure import ProcedureInstance
from .process import ProcessStep, Process


class ProcessInstanceStep(models.Model):
    """Executed Process step. This can be used to track the execution of a process step,
    e.g. in a lab, or in a simulation.
    """

    model_curi = "lara:processes/ProcessInstanceStep"
    processinstancestep_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )

    process_step_base = models.ForeignKey(
        ProcessStep,
        related_name="%(app_label)s_%(class)s_step_classes_related",
        related_query_name="%(app_label)s_%(class)s_step_classes_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="reference to the process step of which this is an instance.",
    )
    process_instance = models.ForeignKey(
        "ProcessInstance",
        related_name="%(app_label)s_%(class)s_process_instance_related",
        related_query_name="%(app_label)s_%(class)s_process_instance_related_query",
        on_delete=models.CASCADE,
        null=False,
        blank=False,
        help_text="reference to the process instance to which this step belongs to",
    )

    status = models.ForeignKey(
        ItemStatus,
        related_name="%(app_label)s_%(class)s_status_related",
        related_query_name="%(app_label)s_%(class)s_status_related_query",
        on_delete=models.CASCADE,
        null=False,
        blank=False,
    )

    datetime_start = models.DateTimeField(
        blank=True, null=True, help_text="start datetime of step"
    )
    datetime_end = models.DateTimeField(
        blank=True, null=True, help_text="end datetime of step"
    )

    datetime_last_modified = models.DateTimeField(
        blank=True, null=True, help_text="date and time when data was last modified"
    )

    procedure_instance = models.ForeignKey(
        "ProcedureInstance",
        related_name="%(app_label)s_%(class)s_procedure_instance_related",
        related_query_name="%(app_label)s_%(class)s_procedure_instance_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="reference to a procedure instance which handles execution of this step",
    )

    parameters = models.JSONField(
        blank=True, null=True, help_text="method/procedure/process parameters"
    )

    simulation = models.BooleanField(
        default=False, help_text="is this a simulation step?"
    )

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["process_instance", "process_step_base"],
                name="%(app_label)s_%(class)s_procedure_instance_name_unique",
            )
        ]

    def __str__(self):
        return (
            f"step:{self.process_instance.name}/[{self.process_step_base.name}] ({self.datetime_start} - {self.datetime_end})"
            or ""
        )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRIs etc.
        """

        if self.datetime_start is None:
            self.datetime_start = timezone.now()

        if self.datetime_last_modified is None:
            self.datetime_last_modified = timezone.now()

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

    def get_duration(self) -> float:
        return (self.datetime_end - self.datetime_start).total_seconds()


class ProcessInstance(ProcessInstanceAbstr):
    """
    A process is a combination of various procedures and decisions,
    e.g. a robot process, a fermentation process, ....
    """

    model_curi = "lara:processes/ProcessInstance"
    processinstance_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    process_base = models.ForeignKey(
        Process,
        related_name="%(app_label)s_%(class)s_process_bases_related",
        related_query_name="%(app_label)s_%(class)s_process_bases_related_query",
        on_delete=models.CASCADE,
        null=False,
        help_text="reference to abstract process",
    )
    process_json = models.JSONField(
        blank=True, null=True, help_text="Process JSON representation"
    )
    process_file = models.FileField(
        upload_to="lara_processes/processes/",
        blank=True,
        null=True,
        help_text="rel. path/filename to processes",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRIs etc.
        """

        if (self.name is None or self.name == "") and self.name_display is not None:
            self.name = (
                self.name_display.strip().replace(" ", "_").lower() + f"_{self.process_base.version}"
            )

        if self.process_base is not None:
            if self.name is None or self.name == "":
                self.name = (
                    f"{datetime.now().strftime('%Y%m%d')}_" + self.process_base.name
                )

            if self.namespace is None or self.namespace == "":
                self.namespace = self.process_base.namespace

            if self.name_display is None or self.name_display == "":
                self.name_display = self.process_base.name_display

            if self.description is None or self.description == "":
                self.description = self.process_base.description

        elif self.name is None or self.name == "":
            self.name = f"{datetime.now().strftime('%Y%m%d')}_{self.process_base.name}"

        if self.iri is None or self.iri == "":
            try:
                self.iri = f"{settings.LARA_PREFIXES['lara']}processes/ProcessInstance/{self.name}"
            except IntegrityError as err:
                self.iri = f"{settings.LARA_PREFIXES['lara']}processes/ProcessInstance/{self.name}/{str(self.processinstance_id)[:8]}"

        if self.datetime_last_modified is None:
            self.datetime_last_modified = timezone.now()

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

# TODO: replace by generic through decorator
class OrderedProcedureInstances(models.Model):
    """Through model for the many-to-many relationship between ProcessInstance and ProcedureInstance.
    This class can be used
    to store the order of procedure instances in a process instance"""

    model_curi = "lara:processes/OrderedProcedureInstances"

    orderedprocedureinstances_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )

    process_instance = models.ForeignKey(
        ProcessInstance,
        related_name="%(app_label)s_%(class)s_process_instance_related",
        related_query_name="%(app_label)s_%(class)s_process_instance",
        on_delete=models.CASCADE,
        help_text="process instance",
    )

    procedure_instance = models.ForeignKey(
        ProcedureInstance,
        related_name="%(app_label)s_%(class)s_procedure_instance_related",
        related_query_name="%(app_label)s_%(class)s_procedure_instance",
        on_delete=models.CASCADE,
        help_text="procedure instance in the process instance",
    )

    order = models.IntegerField(
        help_text="order of procedure instance in process instance"
    )

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["procedure_instance", "process_instance"],
                name="%(app_label)s_%(class)s_procedure_instance_process_instance_unique",
            ),
        ]
        ordering = ("order",)


class ProcessInstancesSubset(ItemSubsetAbstr):
    """
    A subset of process instances, e.g. all process instances for a certain lab, or all process instances for a certain project, ...
    """

    model_curi = "lara:processes/ProcessInstancesSubset"
    processinstancessubset_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    process_instances = models.ManyToManyField(
        ProcessInstance,
        related_name="%(app_label)s_%(class)s_process_instances_related",
        related_query_name="%(app_label)s_%(class)s_process_instances_related_query",
        blank=True,
        help_text="all process instances of a subset",
        through="OrderedProcessInstancesSubset",
    )

    # class Meta:
    #


class OrderedProcessInstancesSubset(models.Model):
    """Through model for the many-to-many relationship between ProcessInstancesSubset and ProcessInstance.
    This class can be used to store the order of process instances in a subset"""

    model_curi = "lara:processes/OrderedProcessInstancesSubset"

    orderedprocessinstancessubset_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )

    process_instance = models.ForeignKey(
        ProcessInstance,
        related_name="%(app_label)s_%(class)s_process_instance_related",
        related_query_name="%(app_label)s_%(class)s_process_instance",
        on_delete=models.CASCADE,
        help_text="process instance in the subset",
    )

    process_instances_subset = models.ForeignKey(
        ProcessInstancesSubset,
        related_name="%(app_label)s_%(class)s_processinstancessubset_related",
        related_query_name="%(app_label)s_%(class)s_processinstancessubset",
        on_delete=models.CASCADE,
        help_text="subset of process instances",
    )

    order = models.IntegerField(help_text="order of process instance in subset")

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["process_instance", "process_instances_subset"],
                name="%(app_label)s_%(class)s_process_instance_processinstancessubset_unique",
            ),
        ]
        ordering = ("order",)
