"""
Base abstract models for lara_django_material.

This module contains abstract base classes used by various material models.
"""

import hashlib
import json
from uuid import uuid4

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, ExtraDataAbstr, Tag
from lara_django_people.models import Entity


class PartShapeAbstr(models.Model):
    """
    Abstract Part / Device / Labware shape class.

    Describes the shape and dimensions of a part/device/labware,
    e.g. SBS.96.round.fb
    """

    name = models.TextField(
        unique=True,
        null=True,
        help_text="name of shape dimension set, e.g. SBS.96.round.fb",
    )
    name_standard = models.TextField(
        blank=True, null=True, help_text="underlying labware/shape standard, e.g. SBS"
    )  # might need an extra table
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    width = models.FloatField(blank=True, null=True, help_text="cont. total width, ")
    length = models.FloatField(blank=True, null=True, help_text="cont. total length ")
    height_unlidded = models.FloatField(blank=True, null=True, help_text="cont. height, without lid")
    hight_lidded = models.FloatField(blank=True, null=True, help_text="cont. height incl. lid")
    hight_stacked = models.FloatField(blank=True, null=True, help_text="stacking height, unlidded")
    hight_stacked_lidded = models.FloatField(blank=True, null=True, help_text="stacking height lidded")
    model_2d = models.TextField(blank=True, null=True, help_text="2D model of part shape in, e.g., SVG format")
    model_3d = models.TextField(blank=True, null=True, help_text="3D model of part shape in, e.g., STL format")
    model_json = models.JSONField(blank=True, null=True, help_text="2D or 3D model of part shape in JSON format")
    description = models.TextField(blank=True, null=True, help_text="description of part shape")

    class Meta:
        abstract = True

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

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


class PartAbstr(models.Model):
    """
    Abstract base class for Parts, Devices, and Labware.

    A Part is an artifact that is used to build a device or perform an experiment
    (screw, nut, bolt, transistor, IC, lamp, diode, ...).

    NOTE: model part/device state in process online, waiting, busy,
    shutting down, off; default: init
    """

    name_display = models.TextField(
        blank=True,
        null=True,
        help_text="human readable name of the part/device/labware for the UI.",
    )
    name = models.TextField(
        blank=True,
        null=True,
        help_text="short part or device name, should not contain whitespaces",
    )
    url = models.URLField(blank=True, null=True, help_text="Universal Resource Locator - URL")
    pid = models.URLField(blank=True, null=True, unique=True, help_text="handle URI")
    # 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.",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    # in case of mulitiple manufacturer, this could be modeled with data_extra
    manufacturer = models.ForeignKey(
        Entity,
        related_name="%(app_label)s_%(class)s_manufacturer_related",
        related_query_name="%(app_label)s_%(class)s_manufacturer_related_query",
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        help_text="manufacturer of the part/device/labware",
    )
    model_no = models.TextField(blank=True, null=True, help_text="model or part number (of e.g. manufacturer)")
    product_type = models.TextField(blank=True, null=True, help_text="(manufacturer) product type")
    # ',UNIQUE(barcode) ON CONFLICT IGNORE )'),
    type_barcode = models.TextField(
        blank=True,
        null=True,
        unique=True,
        help_text="barcode of the general part type, not of a specific part !!.",
    )
    type_barcode_json = models.JSONField(
        blank=True,
        null=True,
        help_text="Advanced barcodes in JSON for multiple barcode representations, like QR codes etc.",
    )
    product_no = models.TextField(blank=True, null=True, help_text="manufacturer product number")
    weight = models.FloatField(blank=True, null=True, help_text="part net weight in kg")
    energy_consumption = models.FloatField(null=True, blank=True, help_text="energy consumption in W")
    spec_json = models.JSONField(blank=True, null=True, help_text="part specifications in JSON format")
    hardware_config = models.JSONField(
        blank=True,
        null=True,
        help_text="Configuration of the item hardware setup, like connection schemata, wiring, tubing, ...",
    )

    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/logo/drawing of the part")
    image = models.ImageField(
        upload_to="material/",
        blank=True,
        null=True,
        help_text="rel. path/filename to image",
    )
    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="e.g. manual",
    )
    datetime_last_modified = models.DateTimeField(
        blank=True, null=True, help_text="date and time when data was last modified"
    )
    description = models.TextField(blank=True, null=True, help_text="description of the device")
    search_vector = SearchVectorField(null=True)

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

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

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


class PartDeviceClass(models.Model):
    """
    Classes for parts and devices.

    Examples: screw, bolt, robot, spectrometer, HPLC, NMR, ...
    """

    model_curi = "lara:material/PartDeviceClass"
    partdeviceclass_id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
    name = models.TextField(
        unique=True,
        help_text="name of the part/device class, like screw, bolt, robot, spectrometer, HPLC, NMR,",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        help_text="International Resource Identifier - IRI: is used for semantic representation in a related ontology",
    )
    description = models.TextField(blank=True, help_text="description")

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

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

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

    class Meta:
        verbose_name_plural = "PartDeviceClasses"
        db_table = "lara_material_part_device_class"


class ExtraData(ExtraDataAbstr):
    """
    Extra information for material models.

    This class can be used to extend material models by extra information,
    e.g., maintenance plan, maintenance history, incidents history,
    image filename, ...
    """

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

    def save(self, *args, **kwargs):
        """Generate default values for version, hash, name, name_full, and IRI."""
        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']}material/ExtraData/{netloc}{path}/{self.name.replace(' ', '_').lower().strip()}"

        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)
