"""_____________________________________________________________________

:PROJECT: LARAsuite

*lara_django_organisms models *

:details: lara_django_organisms database models.
         -
:authors: mark doerr <mark.doerr@uni-greifswald.de>

.. note:: -
.. todo:: -
________________________________________________________________________
"""

import uuid
import hashlib
import json
import logging
from django.utils import timezone
from django.conf import settings
from django.utils.translation import gettext_lazy as _

from django.db import models


from lara_django_base.models import ExtraDataAbstr, PhysicalUnit, Tag

from lara_django_people.models import Entity, LaraUser, ItemSubsetAbstr


settings.FIXTURES += [
    "1_habitat_class_fix",
    "2_domain_fix",
    "3_regnum_fix",
    # "4_phylum_fix",
    # "5_classis_fix",
    # "6_ordo_fix",
    "7_familia_fix",
    "8_genus_fix",
    # "9_species_fix",
    # "10_subspecies_fix",
    # "11_variant_fix",
    # "12_cultivar_fix",
]

logger = logging.getLogger(__name__)


class ExtraData(ExtraDataAbstr):
    """This class can be used to extend data, by extra information,
    e.g. more telephone numbers, customer numbers, ..."""

    model_curi = "lara:organisms/ExtraData"
    file = models.FileField(
        upload_to="organisms", blank=True, null=True, help_text="rel. path/filename"
    )

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

        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.UUID) +
                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']}organisms/ExtraData/{netloc}{path}/{self.name.replace(' ', '_').lower().strip()}"

        self.datetime_last_modified = timezone.now()

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


class HabitatClass(models.Model):
    """classes of habitats, like maritime, terrestrial, shore, .."""

    model_curi = "lara:organisms/HabitatClass"
    habitatclass_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    name = models.TextField(unique=True, help_text="maritime, terrestrial, shore")
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        blank=True, help_text="description of the habitat class"
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/HabitatClass/{self.name.replace(' ', '_').lower().strip()}"

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

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

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

    class Meta:
        verbose_name_plural = "HabitatClasses"
        # ~ db_table = "lara_substances_substance_class"


class HabitatParameter(models.Model):
    """Class level Habitat Parameters can be used to model all kinds of general habitat parameters
    that are common for all organisms of a certain class.
    Habitat parameters of an individual organism should be captured in the
    organism store.
    Examples of these parameters could be moisture, pH, nutrients (?), geological,
     geo_width_min, geo_width_max
     temperature_average
     temperature_min
     temperature_max
     oxygen_concentration_min
     oxygen_concentration_max
     oxygen_concentration_average
     humidity_average
     pH_average

     # salt concentration
     moisture_air_min
     moisture_air_max
     moisture_air_average
     light_during_day_min
     light_during_day_max
     light_during_day_average

     height_min
     height_max
    :TODO: parts should be moved to ontology
    """

    model_curi = "lara:organisms/HabitatParameter"
    habitatparameter_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    name_display = models.TextField(
        help_text="human readable display name of the habitat parameter, can contain spaces"
    )
    name = models.TextField(
        unique=True,
        help_text="name of the habitat parameter, e.g. temperature, moisture, pH, ...",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    value_range = models.JSONField(
        null=True,
        blank=True,
        help_text="range of values for this parameter, like min, max, average, ...",
    )
    unit = models.ForeignKey(
        PhysicalUnit,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_unit_related",
        related_query_name="%(app_label)s_%(class)s_unit_related_query",
        blank=True,
        null=True,
        help_text="physical unit of the parameter, e.g. K, mol/L, ...",
    )
    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. more information about this particular parameter ...",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of the habitat parameter"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat parameter data was last modified",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()

        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/HabitatParameter/{self.name.replace(' ', '_').lower().strip()}"

        self.datetime_last_modified = timezone.now()

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

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

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

    # class Meta:
    #     verbose_name_plural = "HabitatParameters"
    # ~ db_table = "lara_substances_substance_class"


class Habitat(models.Model):
    """Information about the living conditions of an organism, e.g. temperature, moisture, pH, ..."""

    model_curi = "lara:organisms/Habitat"
    habitat_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the habitat, can contain spaces"
    )
    name = models.TextField(
        blank=True,
        null=True,
        help_text="habitat name with namespaces, like sea.shore, sea.deepsee, land.vulcan",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    habitat_class = models.ForeignKey(
        HabitatClass,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_habitat_class_related",
        related_query_name="%(app_label)s_%(class)s_habitat_class_related_query",
        blank=True,
        null=True,
        help_text="class of the habitat, like maritime, terrestrial, shore, ...",
    )
    parameters = models.ManyToManyField(
        HabitatParameter,
        blank=True,
        related_name="%(app_label)s_%(class)s_parameters_related",
        related_query_name="%(app_label)s_%(class)s_parameters_related_query",
        help_text="e.g. more parameters, like nutrients, ...",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of the habitat"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat data was last modified",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/habitat/{self.name.replace(' ', '_').lower().strip()}"

        self.datetime_last_modified = timezone.now()

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

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

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


class Trait(models.Model):
    """Phenotypical, phenomenological or genetic traits / features / attributes of the general organism,
    like branched, round, oval, .. (GRAM) staining, genetic fingerprint sequence,
    or genetic features, like antibiotic resistance,
     morphology
     size
     shape
     motility
     spore
     flagella
     capsule

     growth optimum temp, pH, salt, sugar, ...
     doubling rate
     metabolites
     nutrients
     metabolic characteristics
     pathogenicity
     membrane potential

     legal status / origin / patent
     part of the organism
     culture conditions
    """

    model_curi = "lara:organisms/Trait"
    trait_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the trait, can contain spaces"
    )
    name = models.TextField(
        unique=True,
        help_text="Name of the trait / feature attribute of the organisms, like branched, round, oval, .. staining, genetic fingerprint sequence ",
    )
    value_range = models.JSONField(
        null=True,
        blank=True,
        help_text="range of values for this trait, like min, max, average, ...",
    )
    characteristics = models.JSONField(
        blank=True, null=True, help_text="characteristics of this trait"
    )
    unit = models.ForeignKey(
        PhysicalUnit,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_unit_related",
        related_query_name="%(app_label)s_%(class)s_unit_related_query",
        blank=True,
        null=True,
        help_text="physical unit of the parameter, e.g. m, m^2, V, ...",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of the feature class"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when trait data was last modified",
    )

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

        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()

        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/FeatureClass/{self.name.replace(' ', '_').lower().strip()}"

        self.datetime_last_modified = timezone.now()
        self.datetime_last_modified = timezone.now()

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

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

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

    class Meta:
        verbose_name_plural = "FeatureClasses"


class Domain(models.Model):
    """taxonomic domain / super kingdom / empire: Archaea, Bacteria, and Eukarya"""

    model_curi = "lara:organisms/Domain"
    domain_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the domain, can contain spaces"
    )
    name = models.TextField(
        blank=True, null=True, help_text="domain name: Archaea, Bacteria, and Eukarya"
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    traits = models.ManyToManyField(
        Trait,
        related_name="%(app_label)s_%(class)s_domain_traits_related",
        related_query_name="%(app_label)s_%(class)s_domain_traits",
        blank=True,
        help_text="General domain's traits / features / attributes",
    )
    characteristics = models.JSONField(
        blank=True,
        null=True,
        help_text="General characteristics of Domain. Use 'traits' for specific traits.",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of domain"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat parameter data was last modified",
    )

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

        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()

        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/domain/{self.name.replace(' ', '_').lower().strip()}"
        self.datetime_last_modified = timezone.now()
        super().save(*args, **kwargs)

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

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


class Regnum(models.Model):
    """taxonomic Regnum (Kingdom): Animalia, Plantae, Fungi, Protista,
    Archaea/Archaebacteria, and Bacteria/Eubacteria
    might be deprecated
    """

    model_curi = "lara:organisms/Regnum"
    regnum_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the regnum, can contain spaces"
    )
    name = models.TextField(
        blank=True, null=True, help_text="Regnum name, like Plantae, Fungi"
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    traits = models.ManyToManyField(
        Trait,
        related_name="%(app_label)s_%(class)s_regnum_traits_related",
        related_query_name="%(app_label)s_%(class)s_regnum_traits",
        blank=True,
        help_text="General regnum's traits / features / attributes",
    )
    characteristics = models.JSONField(
        blank=True,
        null=True,
        help_text="General characteristics of Regnum, use 'traits' for specific traits.",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of Regnum"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat parameter data was last modified",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()

        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/regnum/{self.name.replace(' ', '_').lower().strip()}"
        self.datetime_last_modified = timezone.now()
        super().save(*args, **kwargs)

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

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

    class Meta:
        verbose_name_plural = "regna"


class Phylum(models.Model):
    """taxonomic Phylum / Devisio / Division"""

    model_curi = "lara:organisms/Phylum"
    phylum_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the phylum, can contain spaces"
    )
    name = models.TextField(blank=True, null=True, help_text="phylum name, like ")
    name_common = models.TextField(
        blank=True, null=True, help_text="common phylum name, like "
    )
    traits = models.ManyToManyField(
        Trait,
        related_name="%(app_label)s_%(class)s_phylum_traits_related",
        related_query_name="%(app_label)s_%(class)s_phylum_traits",
        blank=True,
        help_text="General phylum's traits / features / attributes",
    )
    characteristics = models.JSONField(
        blank=True,
        null=True,
        help_text="Generic characteristics of phylum. Use 'traits' for specific traits.",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of phylum"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat parameter data was last modified",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()
        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/phylum/{self.name.replace(' ', '_').lower().strip()}"
        self.datetime_last_modified = timezone.now()
        super().save(*args, **kwargs)

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

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

    class Meta:
        verbose_name_plural = "Phyla"


class Classis(models.Model):
    """taxonomic Classis / Class"""

    model_curi = "lara:organisms/Classis"
    classis_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the classis, can contain spaces"
    )
    name = models.TextField(blank=True, null=True, help_text="classis name, like ")
    name_common = models.TextField(
        blank=True, null=True, help_text="common classis name, like "
    )
    traits = models.ManyToManyField(
        Trait,
        related_name="%(app_label)s_%(class)s_classis_traits_related",
        related_query_name="%(app_label)s_%(class)s_classis_traits",
        blank=True,
        help_text="General classis's traits / features / attributes",
    )
    characteristics = models.JSONField(
        blank=True,
        null=True,
        help_text="generic characteristics of this classis, use 'traits' for specific traits.",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of this classis"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat parameter data was last modified",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()
        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/classis/{self.name.replace(' ', '_').lower().strip()}"
        self.datetime_last_modified = timezone.now()
        super().save(*args, **kwargs)

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

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

    class Meta:
        verbose_name_plural = "Classes"


class Ordo(models.Model):
    """taxonomic Ordo / order"""

    model_curi = "lara:organisms/Ordo"
    ordo_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the ordo, can contain spaces"
    )
    name = models.TextField(blank=True, null=True, help_text="ordo name, like ")
    name_common = models.TextField(
        blank=True, null=True, help_text="common ordo name, like "
    )
    traits = models.ManyToManyField(
        Trait,
        related_name="%(app_label)s_%(class)s_ordo_traits_related",
        related_query_name="%(app_label)s_%(class)s_ordo_traits",
        blank=True,
        help_text="General ordo's traits / features / attributes",
    )
    characteristics = models.JSONField(
        blank=True,
        null=True,
        help_text="Generic characteristics of this ordo, use 'traits' for specific traits.",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of this ordo"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat parameter data was last modified",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()
        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/ordo/{self.name.replace(' ', '_').lower().strip()}"
        self.datetime_last_modified = timezone.now()
        super().save(*args, **kwargs)

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

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

    class Meta:
        verbose_name_plural = "Ordae"


class Familia(models.Model):
    """taxonomic Familia / Family"""

    model_curi = "lara:organisms/Familia"
    familia_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the familia, can contain spaces"
    )
    name = models.TextField(blank=True, null=True, help_text="familia name, like ")
    name_common = models.TextField(
        blank=True, null=True, help_text="common familia name, like "
    )
    traits = models.ManyToManyField(
        Trait,
        related_name="%(app_label)s_%(class)s_familia_traits_related",
        related_query_name="%(app_label)s_%(class)s_familia_traits",
        blank=True,
        help_text="General familia's traits / features / attributes",
    )
    characteristics = models.JSONField(
        blank=True,
        null=True,
        help_text="Generic characteristics of this familia, use 'traits' for specific traits.",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of this familia"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat parameter data was last modified",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()
        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/familia/{self.name.replace(' ', '_').lower().strip()}"
        self.datetime_last_modified = timezone.now()
        super().save(*args, **kwargs)

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

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

    class Meta:
        verbose_name_plural = "Familiae"


class Genus(models.Model):
    """taxonomic Genus"""

    model_curi = "lara:organisms/Genus"
    genus_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the genus, can contain spaces"
    )
    name = models.TextField(blank=True, null=True, help_text="genus name, like ")
    name_common = models.TextField(
        blank=True, null=True, help_text="common genus name, like "
    )
    traits = models.ManyToManyField(
        Trait,
        related_name="%(app_label)s_%(class)s_genus_traits_related",
        related_query_name="%(app_label)s_%(class)s_genus_traits",
        blank=True,
        help_text="General genus's traits / features / attributes",
    )
    characteristics = models.JSONField(
        blank=True,
        null=True,
        help_text="General characteristics of this genus, use 'traits' for specific traits.",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of this genus"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat parameter data was last modified",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()
        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/genus/{self.name.replace(' ', '_').lower().strip()}"
        self.datetime_last_modified = timezone.now()
        super().save(*args, **kwargs)

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

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

    class Meta:
        verbose_name_plural = "Genera"


class Species(models.Model):
    """taxonomic Species"""

    model_curi = "lara:organisms/Species"
    species_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the species, can contain spaces"
    )
    name = models.TextField(blank=True, null=True, help_text="species name, like ")
    name_common = models.TextField(
        blank=True, null=True, help_text="common species name, like "
    )
    traits = models.ManyToManyField(
        Trait,
        related_name="%(app_label)s_%(class)s_species_traits_related",
        related_query_name="%(app_label)s_%(class)s_species_traits",
        blank=True,
        help_text="General species's traits / features / attributes",
    )
    characteristics = models.JSONField(
        blank=True,
        null=True,
        help_text="General characteristics of this species, use 'traits' for specific traits.",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of this species"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat parameter data was last modified",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()
        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/species/{self.name.replace(' ', '_').lower().strip()}"
        self.datetime_last_modified = timezone.now()
        super().save(*args, **kwargs)

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

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

    class Meta:
        verbose_name_plural = "Speciae"


class Subspecies(models.Model):
    """taxonomic Subspecies"""

    model_curi = "lara:organisms/Subspecies"
    subspecies_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    name_display = models.TextField(
        help_text="human readable display name of the subspecies, can contain spaces"
    )
    name = models.TextField(blank=True, null=True, help_text="subspecies name, like ")
    name_common = models.TextField(
        blank=True, null=True, help_text="common subspecies name, like "
    )
    traits = models.ManyToManyField(
        Trait,
        related_name="%(app_label)s_%(class)s_subspecies_traits_related",
        related_query_name="%(app_label)s_%(class)s_subspecies_traits",
        blank=True,
        help_text="General subspecies's traits / features / attributes",
    )
    characteristics = models.JSONField(
        blank=True,
        null=True,
        help_text="General characteristics of this subspecies, use 'traits' for specific traits.",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of this subspecies"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat parameter data was last modified",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()
        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/subspecies/{self.name.replace(' ', '_').lower().strip()}"
        self.datetime_last_modified = timezone.now()
        super().save(*args, **kwargs)

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

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

    class Meta:
        verbose_name_plural = "Subspecieae"


class Variant(models.Model):
    """taxonomic Variant"""

    model_curi = "lara:organisms/Variant"
    variant_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the variant, can contain spaces"
    )
    name = models.TextField(blank=True, null=True, help_text="variant name, like ")
    name_common = models.TextField(
        blank=True, null=True, help_text="common variant name, like "
    )
    traits = models.ManyToManyField(
        Trait,
        related_name="%(app_label)s_%(class)s_variant_traits_related",
        related_query_name="%(app_label)s_%(class)s_variant_traits",
        blank=True,
        help_text="General variant's traits / features / attributes",
    )
    characteristics = models.JSONField(
        blank=True,
        null=True,
        help_text="General characteristics of this variant, use 'traits' for specific traits.",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of this variant"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat parameter data was last modified",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()
        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/variant/{self.name.replace(' ', '_').lower().strip()}"
        self.datetime_last_modified = timezone.now()
        super().save(*args, **kwargs)

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

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


class Cultivar(models.Model):
    """taxonomic Cultivar / Plant variety originating from cultivation / cell line"""

    model_curi = "lara:organisms/Cultivar"
    cultivar_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the cultivar, can contain spaces"
    )
    name = models.TextField(blank=True, null=True, help_text="cultivar name, like ")
    name_common = models.TextField(
        blank=True, null=True, help_text="common cultivar name, like "
    )
    traits = models.ManyToManyField(
        Trait,
        related_name="%(app_label)s_%(class)s_cultivar_traits_related",
        related_query_name="%(app_label)s_%(class)s_cultivar_traits",
        blank=True,
        help_text="General cultivar's traits / features / attributes",
    )
    characteristics = models.JSONField(
        blank=True,
        null=True,
        help_text="General characteristics of this cultivar, use 'traits' for specific traits.",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    description = models.TextField(
        null=True, blank=True, help_text="description of this cultivar"
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when habitat parameter data was last modified",
    )

    def save(self, *args, **kwargs):
        """
        Here we generate some default values for IRI
        """
        if self.name is None or self.name == "":
            self.name = self.name_display.replace(" ", "_").lower().strip()
        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/cultivar/{self.name.replace(' ', '_').lower().strip()}"
        self.datetime_last_modified = timezone.now()
        super().save(*args, **kwargs)

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

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


class Organism(models.Model):
    """organism / species / strain  or cell line, details should be in the organism's ontology"""

    model_curi = "lara:organisms/Organism"
    organism_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name_display = models.TextField(
        help_text="human readable display name of the organism, can contain spaces"
    )
    name = models.TextField(
        blank=True,
        null=True,
        help_text="organism name for referencing in code, should not contain whitespaces",
    )
    name_common = models.TextField(
        blank=True, null=True, help_text="common organism name, like 'elephant' "
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    # 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.",
    )
    # taxonomy / classification
    domain = models.ForeignKey(
        Domain,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_organism_domain_related",
        related_query_name="%(app_label)s_%(class)s_organism_domain",
        blank=True,
        null=True,
        help_text="organism's domain name",
    )
    regnum = models.ForeignKey(
        Regnum,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_organism_regnum_related",
        related_query_name="%(app_label)s_%(class)s_organism_regnum",
        blank=True,
        null=True,
        help_text="organism's regnum name",
    )
    phylum = models.ForeignKey(
        Phylum,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_organism_phylum_related",
        related_query_name="%(app_label)s_%(class)s_organism_phylum",
        blank=True,
        null=True,
        help_text="organism's phylum name",
    )
    classis = models.ForeignKey(
        Classis,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_organism_classis_related",
        related_query_name="%(app_label)s_%(class)s_organism_classis",
        blank=True,
        null=True,
        help_text="organism's classis (class) name",
    )
    ordo = models.ForeignKey(
        Ordo,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_organism_ordo_related",
        related_query_name="%(app_label)s_%(class)s_organism_ordo",
        blank=True,
        null=True,
        help_text="organism's ordo name",
    )
    familia = models.ForeignKey(
        Familia,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_organism_familia_related",
        related_query_name="%(app_label)s_%(class)s_organism_familia",
        blank=True,
        null=True,
        help_text="organism's familia (family) name",
    )
    genus = models.ForeignKey(
        Genus,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_organism_genus_related",
        related_query_name="%(app_label)s_%(class)s_organism_genus",
        blank=True,
        null=True,
        help_text="organism's genus name",
    )
    species = models.ForeignKey(
        Species,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_organism_species_related",
        related_query_name="%(app_label)s_%(class)s_organism_species",
        blank=True,
        null=True,
        help_text="organism's species name",
    )
    subspecies = models.ForeignKey(
        Subspecies,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_organism_subspecies_related",
        related_query_name="%(app_label)s_%(class)s_organism_subspecies",
        blank=True,
        null=True,
        help_text="organism's subspecies name",
    )
    variant = models.ForeignKey(
        Variant,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_organism_variant_related",
        related_query_name="%(app_label)s_%(class)s_organism_variant",
        blank=True,
        null=True,
        help_text="organism's varian name",
    )
    cultivar = models.ForeignKey(
        Cultivar,
        on_delete=models.CASCADE,
        related_name="%(app_label)s_%(class)s_organism_cultivar_related",
        related_query_name="%(app_label)s_%(class)s_organism_cultivar",
        blank=True,
        null=True,
        help_text="organism's cultivar name",
    )
    habitats = models.ManyToManyField(
        Habitat,
        related_name="%(app_label)s_%(class)s_organism_habitat_related",
        related_query_name="%(app_label)s_%(class)s_organism_habitat",
        blank=True,
        help_text="general organism's habitats",
    )
    organisms = models.ManyToManyField(
        "self",
        related_query_name="%(app_label)s_%(class)s_organism_organisms",
        blank=True,
        help_text="organisms involved in hybrid or chimera",
    )
    traits = models.ManyToManyField(
        Trait,
        related_name="%(app_label)s_%(class)s_organism_traits_related",
        related_query_name="%(app_label)s_%(class)s_organism_traits",
        blank=True,
        help_text="general organism's traits / features / attributes",
    )
    characteristics = models.JSONField(
        blank=True,
        null=True,
        help_text="General characteristics of this variant, use 'traits' for specific traits.",
    )
    # consider a link to lara-django-sequences to store the genome as a sequence, efficiency needs to be tested
    genome = models.URLField(
        blank=True,
        null=True,
        max_length=512,
        help_text="URL to organism's (reference) genome",
    )

    hybrid = models.BooleanField(
        default=False,
        help_text="Hybrid - organism with mixed genetic material like mule, liger/tigon, plant hybrids.",
    )
    chimera = models.BooleanField(
        default=False,
        help_text="Chimera - organisms with mixed cells from different species, like apple/pear tree.",
    )

    description = models.TextField(
        null=True, blank=True, help_text="description of the organism"
    )

    data_extra = models.ManyToManyField(
        ExtraData,
        related_name="%(app_label)s_%(class)s_organism_extradata_related",
        related_query_name="%(app_label)s_%(class)s_organism_extradata",
        blank=True,
        help_text="organism related extra ",
    )
    tags = models.ManyToManyField(
        Tag,
        related_name="%(app_label)s_%(class)s_organism_tags_related",
        related_query_name="%(app_label)s_%(class)s_organism_tags",
        blank=True,
        help_text="organism related tags",
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when organism data was last modified",
    )

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

        if self.name_display is None or self.name_display == "":
            try:
                self.name_display = (
                    f"{self.genus.name.capitalize()} {self.species.name}"
                )
            except Exception as e:
                logger.error(f"Error in generating name_display for organism: {e}")

        if self.name is None or self.name == "":
            if self.name_display is not None and self.name_display != "":
                self.name = self.name_display.replace(" ", "_").lower().strip()
            else:
                try:
                    self.name = f"{self.genus.name}_{self.species.name}".lower().strip()
                except Exception as e:
                    logger.error(f"Error in generating name for organism: {e}")

        if self.iri is None or self.iri == "":
            self.iri = f"{settings.LARA_PREFIXES['lara']}organisms/organism/{self.name.replace(' ', '_').lower().strip()}"

        self.datetime_last_modified = timezone.now()

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

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

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

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=[
                    "name",
                    "genus",
                    "species",
                    "subspecies",
                    "variant",
                    "cultivar",
                ],
                name="unique_organism",
            )
        ]


class OrganismsSubset(ItemSubsetAbstr):
    """User or group defined subset of organisms"""

    model_curi = "lara:organisms/OrganismsSubset"
    organismssubset_id = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False
    )
    organisms = models.ManyToManyField(
        Organism,
        related_name="%(app_label)s_%(class)s_organisms_related",
        related_query_name="%(app_label)s_%(class)s_organisms",
        blank=True,
        help_text="organisms in this subset",
        through="OrderedOrganismsSubset",
    )


class OrderedOrganismsSubset(models.Model):
    """Through model for the many-to-many relationship between OrganismsSubset and Organism.
    This class can be used to store the order of organisms in a subset"""

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

    organism = models.ForeignKey(
        Organism,
        related_name="%(app_label)s_%(class)s_organism_related",
        related_query_name="%(app_label)s_%(class)s_organism",
        on_delete=models.CASCADE,
        help_text="organism in the subset",
    )

    organisms_subset = models.ForeignKey(
        OrganismsSubset,
        related_name="%(app_label)s_%(class)s_organismssubset_related",
        related_query_name="%(app_label)s_%(class)s_organismssubset",
        on_delete=models.CASCADE,
        help_text="subset of organisms",
    )

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

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["organism", "organisms_subset"],
                name="%(app_label)s_%(class)s_organisms_organisms_subset_unique",
            ),
        ]
        ordering = ("order",)
