"""_____________________________________________________________________

:PROJECT: LARAsuite

*lara_django_processes models for method*

:details: lara_django_processes database models.

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

.. note:: -
.. todo:: - remove unwanted models/fields
________________________________________________________________________
"""

import uuid

from django.conf import settings
# unique constraint exception
from django.db import IntegrityError
from django.db import models
from django.utils import timezone

from lara_django_people.models import ItemSubsetAbstr

from .abstracts import ProcessAbstr, ProcMethodClass, ProcessInstanceAbstr

class Method(ProcessAbstr):
    """
    Def. Method: A process by which a task is completed;
    a way of doing something (followed by the adposition of, to or for before the purpose of the process).
    E.g. method chromatography.HPLC , GC, NMR, PCR, ...
    This is a generic method representation, for a concrete Method in a particular lab, see MethodInstance
    """

    model_curi = "lara:processes/Method"
    method_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    method_class = models.ForeignKey(
        ProcMethodClass,
        related_name="%(app_label)s_%(class)s_method_classes_related",
        related_query_name="%(app_label)s_%(class)s_method_classes_related_query",
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        help_text="class of the method",
    )
    method_json = models.JSONField(
        blank=True, null=True, help_text="Method JSON representation"
    )

    def save(self, *args, **kwargs):
        # ~ if not self.slug :
        # ~ self.slug = '-'.join((self.proc_class, self.name, self.version)).lower()

        if self.version is None or self.version == "":
            self.version = "0.0.1"

        if self.uri == "":
            self.uri = None

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

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

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

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

# TODO: replace by generic decorator
# @with_through_model_fields({
#     'methods': MethodSubset,
# })
class MethodsSubset(ItemSubsetAbstr):
    """
    A subset of methods, e.g. all methods for a certain lab, or all methods for a certain project, ...
    """

    model_curi = "lara:processes/MethodsSubset"
    methodssubset_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    methods = models.ManyToManyField(
        Method,
        related_name="%(app_label)s_%(class)s_methods_related",
        related_query_name="%(app_label)s_%(class)s_methods_related_query",
        blank=True,
        help_text="all methods of a subset",
        through="OrderedMethodsSubset",
    )

    # class Meta:
    #     verbose_name_plural = "MethodsSubsets"


class OrderedMethodsSubset(models.Model):
    """Through model for the many-to-many relationship between MethodsSubset and Method.
    This class can be used to store the order of methods in a subset"""

    model_curi = "lara:processes/OrderedMethodsSubset"

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

    method = models.ForeignKey(
        Method,
        related_name="%(app_label)s_%(class)s_method_related",
        related_query_name="%(app_label)s_%(class)s_method",
        on_delete=models.CASCADE,
        help_text="method in the subset",
    )

    methods_subset = models.ForeignKey(
        MethodsSubset,
        related_name="%(app_label)s_%(class)s_methodssubset_related",
        related_query_name="%(app_label)s_%(class)s_methodssubset",
        on_delete=models.CASCADE,
        help_text="subset of methods",
    )

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

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["method", "methods_subset"],
                name="%(app_label)s_%(class)s_method_methodssubset_unique",
            ),
        ]
        ordering = ("order",)

class MethodInstance(ProcessInstanceAbstr):
    """
    A process by which a task is completed;
    a way of doing something (followed by the adposition of, to or for before the purpose of the process).
    E.g. chromatography.HPLC, GC, NMR, PCR, ...
    This is concrete instance of a method
    """

    model_curi = "lara:processes/MethodInstance"
    methodinstance_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    method_base = models.ForeignKey(
        Method,
        related_name="%(app_label)s_%(class)s_method_base_related",
        related_query_name="%(app_label)s_%(class)s_method_base_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="reference method = referring method class, like chromatography.HPLC",
    )

    method_json = models.JSONField(
        blank=True, null=True, help_text="Method JSON representation"
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRIs etc.
        """
        if self.method_base is not None:
            # iterate over all fields of the method_base and copy them to the method_instance, but only, if they are not set
            try:
                for field in self.method_base._meta.fields:
                    if getattr(self, field.name) is None:
                        setattr(self, field.name, getattr(self.method_base, field.name))
            except AttributeError as e:
                pass  # missing field in method_base, e.g. method_id

        if self.version is None or self.version == "":
            self.version = "0.0.1"

        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.version}"
            )
        if self.name is None or self.name == "":
            self.name = f"{datetime.now().strftime('%Y%m%d')}_" + self.method_base.name

        if self.method_base is not None:
            if self.namespace is None or self.namespace == "":
                self.namespace = self.method_base.namespace

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

            if self.description is None or self.description == "":
                self.description = self.method_base.description
        elif self.name is None or self.name == "":
            self.name = f"{datetime.now().strftime('%Y%m%d')}_method"

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

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

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


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

    model_curi = "lara:processes/MethodInstancesSubset"
    methodinstancessubset_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    method_instances = models.ManyToManyField(
        MethodInstance,
        related_name="%(app_label)s_%(class)s_method_instances_related",
        related_query_name="%(app_label)s_%(class)s_method_instances_related_query",
        blank=True,
        help_text="all method instances of a subset",
        through="OrderedMethodInstancesSubset",
    )

    # class Meta:
    #


class OrderedMethodInstancesSubset(models.Model):
    """Through model for the many-to-many relationship between MethodInstancesSubset and MethodInstance.
    This class can be used to store the order of method instances in a subset"""

    model_curi = "lara:processes/OrderedMethodInstancesSubset"

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

    method_instance = models.ForeignKey(
        MethodInstance,
        related_name="%(app_label)s_%(class)s_method_instance_related",
        related_query_name="%(app_label)s_%(class)s_method_instance",
        on_delete=models.CASCADE,
        help_text="method instance in the subset",
    )

    method_instances_subset = models.ForeignKey(
        MethodInstancesSubset,
        related_name="%(app_label)s_%(class)s_methodinstancessubset_related",
        related_query_name="%(app_label)s_%(class)s_methodinstancessubset",
        on_delete=models.CASCADE,
        help_text="subset of method instances",
    )

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

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["method_instance", "method_instances_subset"],
                name="%(app_label)s_%(class)s_method_instance_methodinstancessubset_unique",
            ),
        ]
        ordering = ("order",)