"""_____________________________________________________________________

:PROJECT: LARAsuite

*lara_django_processes model abstracts and extra data*

: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 hashlib
import json
import uuid

from django.conf import settings
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField
from django.db import models
from django.utils import timezone

from lara_django_base.models import ExternalResource
from lara_django_base.models import ExtraDataAbstr
from lara_django_base.models import ItemStatus
from lara_django_base.models import ItemRole
from lara_django_base.models import MediaType
from lara_django_base.models import Namespace
from lara_django_base.models import Tag
from lara_django_material.models import Device
from lara_django_material.models import DeviceSetup
from lara_django_material.models import Labware
from lara_django_material.models import Part
from lara_django_material_store.models import DeviceInstance
from lara_django_material_store.models import DeviceSetupInstance
from lara_django_material_store.models import LabwareInstance
from lara_django_material_store.models import PartInstance
from lara_django_organisms.models import Organism
from lara_django_organisms_store.models import OrganismInstance
from lara_django_people.models import Entity
from lara_django_samples.models import Sample
from lara_django_samples.models import SampleSet
from lara_django_substances.models import Mixture
from lara_django_substances.models import Polymer
from lara_django_substances.models import Substance
from lara_django_substances_store.models import MixtureInstance
from lara_django_substances_store.models import PolymerInstance
from lara_django_substances_store.models import SubstanceInstance
from lara_django_processes.through_models_decorator import with_through_model_fields

class ExtraData(ExtraDataAbstr):
    """This class can be used to extend the process data, by extra information,
    e.g.,   ..."""

    model_curi = "lara:processes/ExtraData"
    image = models.ImageField(
        upload_to="processes/images/",
        blank=True,
        null=True,  # default="image.svg",
        help_text="location room map rel. path/filename to image",
    )
    file = models.FileField(
        upload_to="processes/data_extra/",
        blank=True,
        null=True,
        help_text="rel. path/filename",
    )

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

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

        if self.hash_sha256 is None:
            if self.data_json is not None:
                to_hash = json.dumps(self.data_json)  # +  str(self.extradata_id) +
                self.hash_sha256 = hashlib.sha256(to_hash.encode("utf-8")).hexdigest()
            else:
                self.hash_sha256 = hashlib.sha256(
                    str(self.extradata_id).encode("utf-8")
                ).hexdigest()

        if self.name is None or self.name == "":
            if self.name_display is None or self.name_display == "":
                self.name = "_".join(
                    (
                        timezone.now().strftime("%Y%m%d_%H%M%S"),
                        "data",
                        self.hash_sha256[:8],
                    )
                )
            else:
                self.name = self.name_display.replace(" ", "_").lower().strip()

        else:
            self.name = self.name.replace(" ", "_").lower().strip()
        if self.name_full is None or self.name_full == "":
            if self.namespace is not None and self.version is not None:
                self.name_full = f"{self.namespace.uri}/" + "_".join(
                    (self.name.replace(" ", "_"), self.version)
                )
            else:
                self.name_full = "_".join((self.name.replace(" ", "_"), self.version))

        if not self.iri:
            netloc = self.namespace.parse_URI().netloc
            path = self.namespace.parse_URI().path

            self.iri = f"{settings.LARA_PREFIXES['lara']}processes/ExtraData/{netloc}{path}/{self.name.replace(' ', '_').lower().strip()}"  # noqa: E501

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

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

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


class ProcMethodClass(models.Model):
    """classes for processes / methods / procedures ..."""

    model_curi = "lara:processes/ProcMethodClass"
    procmethodclass_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    name = models.TextField(unique=True, help_text="name of the method class, ")
    iri = models.TextField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    proc_method_schema = models.JSONField(
        blank=True,
        null=True,
        help_text="schema of the method/procedure/process class and their corresponding instances",
    )
    tags = models.ManyToManyField(
        Tag,
        related_name="%(app_label)s_%(class)s_tags_related",
        related_query_name="%(app_label)s_%(class)s_tags_related_query",
        blank=True,
        help_text="tags",
    )
    description = models.TextField(blank=True, help_text="description")

    class Meta:
        verbose_name_plural = "MethodClasses"
        # ~ db_table = "lara_method_class"

    def __str__(self):
        return self.name or ""

@with_through_model_fields({
    'substances': ItemRole,
    'polymers': ItemRole,
    'mixtures': ItemRole,
    'parts': ItemRole,
    'labware': ItemRole,
    'organisms': ItemRole,
    'samples': ItemRole,
    'sample_sets': ItemRole,
})
class ProcessAbstr(models.Model):
    """
    Abstract base class of all process-like classes (methods/procedures/processes)
    """

    model_curi = "lara:processes/ProcessAbstr"
    namespace = models.ForeignKey(
        Namespace,
        related_name="%(app_label)s_%(class)s_namespaces_related",
        related_query_name="%(app_label)s_%(class)s_namespaces_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="namespace of process/method/procedure/",
    )
    name_display = models.TextField(
        blank=True, help_text="human readable name of the process/method/procedure."
    )

    name = models.TextField(
        blank=True, help_text="name of the method/procedure/process"
    )
    uri = models.URLField(
        # unique=True,
        blank=True,
        null=True,
        max_length=512,
        help_text="Uniform Resource Identifier to locate method/procedure/process",
    )
    iri = models.TextField(
        blank=True,
        null=True,
        unique=True,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    pid = models.TextField(
        blank=True,
        null=True,
        unique=True,
        help_text="Persistent Identifier [PID/handle URI](https://www.handle.net/)",
    )
    # PAC-ID:  https://github.com/ApiniLabs/PAC-ID
    pac_id = models.URLField(
        blank=True,
        null=True,
        unique=True,
        help_text="Publicly Addressable Content IDentifier is a globally unique \
                   identifier that operates independently of a central registry.",
    )
    version = models.TextField(
        blank=True,
        help_text="maj.min.patch version of this method/procedure/process, e.g. 0.2.3",
    )
    datetime_last_modified = models.DateTimeField(
        blank=True, null=True, help_text="date and time when data was last modified"
    )
    parameters = models.JSONField(
        blank=True, null=True, help_text="method/procedure/process parameters"
    )
    parameters_schema = models.JSONField(
        blank=True,
        null=True,
        help_text="schema of the parameters for the method/procedure/process parameters and their corresponding instances",
    )
    substances = models.ManyToManyField(
        Substance,
        related_name="%(app_label)s_%(class)s_substances_related",
        related_query_name="%(app_label)s_%(class)s_substances_related_query",
        blank=True,
        help_text="Substance classes used, e.g. for chemical synthesis,",
    )
    polymers = models.ManyToManyField(
        Polymer,
        related_name="%(app_label)s_%(class)s_polymers_related",
        related_query_name="%(app_label)s_%(class)s_polymers_related_query",
        blank=True,
        help_text="Polymer classes used ",
    )
    mixtures = models.ManyToManyField(
        Mixture,
        related_name="%(app_label)s_%(class)s_mixtures_related",
        related_query_name="%(app_label)s_%(class)s_mixtures_related_query",
        blank=True,
        help_text="Substance Mixture Classes used in the method/procedure/process",
    )
    parts = models.ManyToManyField(
        Part,
        related_name="%(app_label)s_%(class)s_parts_related",
        related_query_name="%(app_label)s_%(class)s_parts_related_query",
        blank=True,
        help_text="Part classes used in this method/procedure/process",
    )
    devices = models.ManyToManyField(
        Device,
        related_name="%(app_label)s_%(class)s_devices_related",
        related_query_name="%(app_label)s_%(class)s_devices_related_query",
        blank=True,
        help_text="Devices classes used in this method/procedure/process",
    )
    device_setups = models.ManyToManyField(
        DeviceSetup,
        related_name="%(app_label)s_%(class)s_devices_setups_related",
        related_query_name="%(app_label)s_%(class)s_devices_setups_related_query",
        blank=True,
        help_text="DeviceSetup classes used in this method/procedure/process",
    )
    labware = models.ManyToManyField(
        Labware,
        related_name="%(app_label)s_%(class)s_containers_related",
        related_query_name="%(app_label)s_%(class)s_containers_related_query",
        blank=True,
        help_text="Labware classes",
    )
    organisms = models.ManyToManyField(
        Organism,
        related_name="%(app_label)s_%(class)s_organisms_related",
        related_query_name="%(app_label)s_%(class)s_organisms_related_query",
        blank=True,
        help_text="Organisms class used in this method/procedure/process",
    )
    samples = models.ManyToManyField(
        Sample,
        related_name="%(app_label)s_%(class)s_samples_related",
        related_query_name="%(app_label)s_%(class)s_samples_related_query",
        blank=True,
        help_text="Samples used in this method/procedure/process",
    )
    sample_sets = models.ManyToManyField(
        SampleSet,
        related_name="%(app_label)s_%(class)s_sample_sets_related",
        related_query_name="%(app_label)s_%(class)s_sample_sets_related_query",
        blank=True,
        help_text="SampleSets used in this method/procedure/process",
    )
    creators = models.ManyToManyField(
        Entity,
        related_name="%(app_label)s_%(class)s_creators_related",
        related_query_name="%(app_label)s_%(class)s_creators_related_query",
        blank=True,
        help_text="creators of method/procedure/process",
    )
    resources_external = models.ManyToManyField(
        ExternalResource,
        related_name="%(app_label)s_%(class)s_external_resources_related",
        related_query_name="%(app_label)s_%(class)s_external_resources_related_query",
        blank=True,
        help_text="external resources, like literature, websites, manuals, ...",
    )
    icon = models.TextField(
        blank=True, null=True, help_text="XML/SVG icon / symbol of the method"
    )
    data_extra = models.ManyToManyField(
        ExtraData,
        related_name="%(app_label)s_%(class)s_data_extra_related",
        related_query_name="%(app_label)s_%(class)s_data_extra_related_query",
        blank=True,
        help_text="extra data",
    )
    media_type = models.ForeignKey(
        MediaType,
        related_name="%(app_label)s_%(class)s_media_types_related",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="file type of method file",
    )
    tags = models.ManyToManyField(
        Tag,
        related_name="%(app_label)s_%(class)s_tags_related",
        related_query_name="%(app_label)s_%(class)s_tags_related_query",
        blank=True,
        help_text="tags",
    )
    description = models.TextField(blank=True, null=True, help_text="")
    remarks = models.TextField(blank=True, null=True, help_text="")

    search_vector = SearchVectorField(null=True)

    def __str__(self):
        return self.name or ""

    def __repr__(self):
        return self.name or ""

    class Meta:
        abstract = True
        constraints = [
            models.UniqueConstraint(
                fields=["name", "namespace", "version"],
                name="%(app_label)s_%(class)s_namespace_name_version_unique",
            )
        ]
        indexes = (GinIndex(fields=["search_vector"]),)

@with_through_model_fields({
    'substance_instances': ItemRole,
    'polymer_instances': ItemRole,
    'mixture_instances': ItemRole,
    'part_instances': ItemRole,
    'labware_instances': ItemRole,
    'device_instances': ItemRole,
    'device_setup_instances': ItemRole,
    'organism_instances': ItemRole,
    'samples': ItemRole,
    'sample_sets': ItemRole,
})
class ProcessInstanceAbstr(models.Model):
    """
    Abstract base class of all method-like classes
    """

    model_curi = "lara:processes/ProcessInstanceAbstr"
    namespace = models.ForeignKey(
        Namespace,
        related_name="%(app_label)s_%(class)s_namespaces_related",
        related_query_name="%(app_label)s_%(class)s_namespaces_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="namespace of method/procedure/process",
    )
    name_display = models.TextField(
        blank=True, help_text="human readable name of the method/procedure/process"
    )

    name = models.TextField(
        unique=True, blank=True, help_text="name of the method/procedure/process"
    )

    uri = models.URLField(
        unique=True,
        blank=True,
        null=True,
        max_length=512,
        help_text="Uniform Resource Identifier to locate method/procedure/process",
    )
    datetime_start = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when the Method/Produre/Process was started",
    )
    datetime_end = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when the Method/Produre/Process was finished",
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when the Method/Produre/Process was last modified",
    )
    parameters = models.JSONField(
        blank=True, null=True, help_text="method/procedure/process parameters"
    )
    version = models.TextField(
        blank=True,
        null=True,
        help_text="maj.min.patch version of this data set, e.g. 0.0.1",
    )

    status = models.ForeignKey(
        ItemStatus,
        related_name="%(app_label)s_%(class)s_namespaces_related",
        related_query_name="%(app_label)s_%(class)s_namespaces_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="state of the method/procedure/process",
    )
    substance_instances = models.ManyToManyField(
        SubstanceInstance,
        related_name="%(app_label)s_%(class)s_substances_related",
        related_query_name="%(app_label)s_%(class)s_substances_related_query",
        blank=True,
        help_text="S e.g. for chemical synthesis,",
    )
    polymer_instances = models.ManyToManyField(
        PolymerInstance,
        related_name="%(app_label)s_%(class)s_polymers_related",
        related_query_name="%(app_label)s_%(class)s_polymers_related_query",
        blank=True,
        help_text="Polymers used e.g. for sequencing ",
    )
    mixture_instances = models.ManyToManyField(
        MixtureInstance,
        related_name="%(app_label)s_%(class)s_mixtures_related",
        related_query_name="%(app_label)s_%(class)s_mixtures_related_query",
        blank=True,
        help_text="Mixture instances used",
    )
    part_instances = models.ManyToManyField(
        PartInstance,
        related_name="%(app_label)s_%(class)s_parts_related",
        related_query_name="%(app_label)s_%(class)s_parts_related_query",
        blank=True,
        help_text="Part instances used in this method/procedure/process",
    )
    labware_instances = models.ManyToManyField(
        LabwareInstance,
        related_name="%(app_label)s_%(class)s_containers_related",
        related_query_name="%(app_label)s_%(class)s_containers_related_query",
        blank=True,
        help_text="Labware instances used in this method/procedure/process",
    )
    device_instances = models.ManyToManyField(
        DeviceInstance,
        related_name="%(app_label)s_%(class)s_devices_related",
        related_query_name="%(app_label)s_%(class)s_devices_related_query",
        blank=True,
        help_text="Devices used in this method/procedure/process",
    )
    device_setup_instances = models.ManyToManyField(
        DeviceSetupInstance,
        related_name="%(app_label)s_%(class)s_device_setups_related",
        related_query_name="%(app_label)s_%(class)s_device_setups_related_query",
        blank=True,
        help_text="DeviceSetup instances used in this method/procedure/process",
    )
    organism_instances = models.ManyToManyField(
        OrganismInstance,
        related_name="%(app_label)s_%(class)s_organisms_related",
        related_query_name="%(app_label)s_%(class)s_organisms_related_query",
        blank=True,
        help_text="Organism instances used in this method/procedure/process",
    )
    samples = models.ManyToManyField(
        Sample,
        related_name="%(app_label)s_%(class)s_samples_related",
        related_query_name="%(app_label)s_%(class)s_samples_related_query",
        blank=True,
        help_text="Samples used in this method/procedure/process",
    )
    sample_sets = models.ManyToManyField(
        SampleSet,
        related_name="%(app_label)s_%(class)s_sample_sets_related",
        related_query_name="%(app_label)s_%(class)s_sample_sets_related_query",
        blank=True,
        help_text="SampleSets used in this method/procedure/process",
    )

    creators = models.ManyToManyField(
        Entity,
        related_name="%(app_label)s_%(class)s_creators_related",
        related_query_name="%(app_label)s_%(class)s_creators_related_query",
        blank=True,
        help_text="creators of method/procedure/process",
    )
    resources_external = models.ManyToManyField(
        ExternalResource,
        related_name="%(app_label)s_%(class)s_external_resources_related",
        related_query_name="%(app_label)s_%(class)s_external_resources_related_query",
        blank=True,
        help_text="external resources, like literature, websites, manuals, ...",
    )
    icon = models.TextField(
        blank=True, null=True, help_text="XML/SVG icon / symbol of the method"
    )
    tags = models.ManyToManyField(
        Tag,
        related_name="%(app_label)s_%(class)s_tags_related",
        related_query_name="%(app_label)s_%(class)s_tags_related_query",
        blank=True,
        help_text="tags",
    )
    data_extra = models.ManyToManyField(
        ExtraData,
        related_name="%(app_label)s_%(class)s_data_extra_related",
        related_query_name="%(app_label)s_%(class)s_data_extra_related_query",
        blank=True,
        help_text="e.g. manual",
    )
    description = models.TextField(blank=True, null=True, help_text="")
    remarks = models.TextField(blank=True, null=True, help_text="")
    iri = models.TextField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    media_type = models.ForeignKey(
        MediaType,
        related_name="%(app_label)s_%(class)s_media_types_related",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="file type of method file",
    )
    pid = models.TextField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="Persitent Identifier [PID/handle URI](https://www.handle.net/)",
    )

    search_vector = SearchVectorField(null=True)

    class Meta:
        abstract = True
        constraints = [
            models.UniqueConstraint(
                fields=["name", "namespace", "version"],
                name="%(app_label)s_%(class)s_namespace_name_version_unique",
            )
        ]
        indexes = (GinIndex(fields=["search_vector"]),)

    def __str__(self):
        return self.name or ""

    def __repr__(self):
        return self.name or ""

    def duration(self):
        if self.datetime_start and self.datetime_end:
            return self.datetime_end - self.datetime_start
        else:
            return None
