"""
_____________________________________________________________________.

:PROJECT: LARAsuite

*lara_django_store models *

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

________________________________________________________________________
"""

import datetime
import logging
import uuid
from typing import Any

from django.conf import settings
from django.contrib.postgres.indexes import GinIndex
from django.contrib.postgres.search import SearchVectorField
from django.core.exceptions import ValidationError
from django.db import models
from django.utils import timezone
from lara_django_base.models import Currency, ExternalResource, ItemStatus, Location, Namespace, Tag
from lara_django_people.models import Entity, Group


class HazardClass(models.Model):
    """
    (Bio-)Hazard Class Model for substances, polymers, mixtures, ... .

    implements the hazard classification  Globally Harmonized System of Classification and Labelling of Chemicals (GHS)
    Standard signal word, hazard classes, pictograms, ...
    """

    model_curi = "lara:store/HazardClass"
    hazardclass_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    name = models.TextField(unique=True, default="", help_text="name of the hazard class")
    signal_word = models.TextField(blank=True, default="", help_text="signal word of the hazard class")
    description = models.TextField(blank=True, default="", help_text="description of the hazard class")
    pictogram_svg = models.TextField(
        blank=True,
        help_text="pictogram of the hazard class",
    )
    pictogram = models.ImageField(
        upload_to="substances_store/images/",
        blank=True,
        null=True,
        help_text="pictogram of the hazard class",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )

    def __str__(self) -> str:
        """Return string representation."""
        return self.name or ""

    def __repr__(self) -> str:
        """Return repr representation."""
        return self.name or ""

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

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


class SafetyInformation(models.Model):
    """
    Safety Information Model for substances, polymers, mixtures, ... .

    implements the Safety Information part of the Globally Harmonized System of Classification and Labelling of
    Chemicals (GHS) Standard hazard statements, precautionary statements
    """

    model_curi = "lara:store/SafetyInformation"
    safetyinformation_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    code = models.TextField(unique=True, help_text="code / unique identifier of the hazard class")
    phrase = models.TextField(blank=True, help_text="phrase of the hazard class")
    hazard_statement = models.BooleanField(default=False, help_text="Is safety hazard statement ?.")
    precautionary_statement = models.BooleanField(default=False, help_text="Is safety precautionary statement ?.")
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    search_vector = SearchVectorField(null=True)

    class Meta:
        """Meta options for SafetyInformation model."""

        indexes = (GinIndex(fields=["search_vector"]),)

    def __str__(self) -> str:
        """Return string representation."""
        return self.code or ""

    def __repr__(self) -> str:
        """Return repr representation."""
        return self.code or ""

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

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


class ItemInstanceAbstr(models.Model):
    """Abstract Item Instance Model fitting for parts, devices, labware, chemicals, organisms, etc."""

    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 item",
    )
    name_display = models.TextField(blank=True, help_text="Human readable name of item, can contain spaces")
    name = models.TextField(
        blank=True,
        help_text="short item name for accessing the item, spaces are replaced by _",
    )
    barcode = models.TextField(
        blank=True,
        unique=True,
        help_text="text representation of a barcode of the item.",
    )
    barcode_json = models.JSONField(
        blank=True,
        null=True,
        help_text="Advanced barcodes in JSON for multiple barcode representations, like QR codes etc.",
    )
    url = models.URLField(blank=True, help_text="Universal Resource Locator - URL")
    pid = models.URLField(blank=True, null=True, unique=True, help_text="Persistent Identifier URI")
    # PAC-ID:  https://github.com/ApiniLabs/PAC-ID
    pac_id = models.CharField(
        blank=True,
        null=True,
        unique=True,
        max_length=255,
        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.",
    )
    registration_no = models.TextField(
        blank=True,
        help_text="can be used for internal registrations in a labstore",
    )
    vendor = models.ForeignKey(
        Entity,
        related_name="%(app_label)s_%(class)s_vendors_related",
        related_query_name="%(app_label)s_%(class)s_vendors_related_query",
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        help_text="vendor of the device",
    )
    product_id = models.TextField(blank=True, help_text="vendor product id")
    serial_no = models.TextField(blank=True, help_text="part/devices serial number")
    service_no = models.TextField(blank=True, help_text="service number or tag")
    datetime_manufacturing = models.DateTimeField(null=True, blank=True, help_text="date of manufaction")
    # price - try also https://github.com/django-money/django-money
    price = models.DecimalField(
        blank=True,
        null=True,
        max_digits=14,
        decimal_places=5,
        help_text="price of the part in default currency/EURO",
    )
    currency = models.ForeignKey(
        Currency,
        related_name="%(app_label)s_%(class)s_currencies_related",
        related_query_name="%(app_label)s_%(class)s_currencies_related_query",
        on_delete=models.CASCADE,
        blank=True,
        null=True,
        help_text="currency of the item price",
    )
    charges = models.JSONField(
        blank=True,
        null=True,
        help_text="charges for the item, like toll, but also discounts ...",
    )
    datetime_purchase = models.DateTimeField(null=True, blank=True, help_text="Date of item purchase")
    datetime_delivery = models.DateTimeField(
        blank=True, null=True, help_text="Date of Item delivery = inventarisation date"
    )
    datetime_end_of_warranty = models.DateTimeField(null=True, blank=True, help_text="")
    amount = models.PositiveIntegerField(null=True, blank=True, help_text="amount of items")
    location = models.ForeignKey(
        Location,
        related_name="%(app_label)s_%(class)s_locations_related",
        related_query_name="%(app_label)s_%(class)s_locations_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="location of container",
    )
    storage_info = models.JSONField(
        blank=True,
        null=True,
        help_text="storage information, like temperature, humidity, light,  date opened, date_best_before  ...",
    )
    safety_info = models.JSONField(
        blank=True,
        null=True,
        help_text="safety information, like hazard classes, statements, ...",
    )
    legal_info = models.JSONField(
        blank=True,
        null=True,
        help_text="legal information, like restrictions, licenses, origins of organisms,  ...",
    )
    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=True,
        blank=True,
        help_text="status of item",
    )
    users = models.ManyToManyField(
        Entity,
        related_name="%(app_label)s_%(class)s_entities_related",
        related_query_name="%(app_label)s_%(class)s_entities_related_query",
        blank=True,
        help_text="people, who are using and taking care of this item",
    )
    entities_responsible = models.ManyToManyField(
        Entity,
        related_name="%(app_label)s_%(class)s_entities_responsible_related",
        related_query_name="%(app_label)s_%(class)s_entities_responsible_related_query",
        blank=True,
        help_text="entities who are mainly responsible for this item",
    )
    entities_own = models.ManyToManyField(
        Entity,
        related_name="%(app_label)s_%(class)s_entities_own_related",
        related_query_name="%(app_label)s_%(class)s_entities_own_related_query",
        blank=True,
        help_text="entities, who are owning this item",
    )
    groups = models.ManyToManyField(
        Group,
        related_name="%(app_label)s_%(class)s_groups_related",
        related_query_name="%(app_label)s_%(class)s_groups_related_query",
        blank=True,
        help_text="groups of users/entities who can access / and or use this item",
    )
    hash_sha256 = models.CharField(
        max_length=256,
        blank=True,
        help_text="SHA256 hash of item for identity/duplicate checking",
    )
    color = models.TextField(
        blank=True,
        # default=ColorBlind16.full_color_scheme[randint(0, 16)].get_web(),
        help_text="GUI colour representation of the item",
    )
    tags = models.ManyToManyField(
        Tag,
        blank=True,
        related_name="%(app_label)s_%(class)s_tags_related",
        related_query_name="%(app_label)s_%(class)s_tags_related_query",
        help_text="tags",
    )
    # TODO: add role to external resources (as through model)
    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, ...",
    )
    description = models.TextField(blank=True, help_text="description of part instance")
    hardware_config = models.JSONField(
        blank=True,
        null=True,
        help_text="Configuration of the item hardware setup, like connection schemata, wiring, tubing, ...",
    )
    software_config = models.JSONField(
        null=True,
        blank=True,
        help_text="(Internal) Software/Firmware configuration of the item, like interface setup,"
        "ports, DNS, IP, MAC, initialisation parameters, SiLA, OPC-UA configuration, ... ",
    )
    datetime_last_modified = models.DateTimeField(
        blank=True,
        null=True,
        help_text="date and time when data was last modified",  # default=timezone.now,
    )

    search_vector = SearchVectorField(null=True)

    class Meta:
        """Meta options for ItemInstanceAbstr model."""

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

    def __str__(self) -> str:  # noqa: D105
        return self.name_full or ""

    def __repr__(self) -> str:  # noqa: D105
        return self.name_full or ""

    def warranty_left(self, day_interval: int = 7) -> float | None:
        """Warranty left in days. Choose day_interval = 7 for weeks."""
        try:
            now = datetime.datetime.now()
            # datetime object is required for subtraction
            warranty_end = datetime.datetime.combine(self.datetime_end_of_warranty, datetime.datetime.min.time())
            warranty_left = warranty_end - now

            return warranty_left.days / day_interval
        except Exception:
            logging.exception("Error in warranty_left")
            return None

    warranty_left.admin_order_field = "datetime_end_of_warranty"  # type: ignore[attr-defined]
    warranty_left.short_description = "Warranty left [weeks]"  # type: ignore[attr-defined]

    def end_of_warranty_soon(self) -> bool | None:  # noqa: D102
        try:
            now = datetime.datetime.now()
            warranty_end = datetime.datetime.combine(self.datetime_end_of_warranty, datetime.datetime.min.time())
            warranty_left = warranty_end - now

            return warranty_left < datetime.timedelta(weeks=12)
        except Exception:
            logging.exception("Error in end_of_warranty_soon")
            return None

    end_of_warranty_soon.admin_order_field = "datetime_end_of_warranty"  # type: ignore[attr-defined]
    end_of_warranty_soon.boolean = True  # type: ignore[attr-defined]
    end_of_warranty_soon.short_description = "Warranty ends soon ?"  # type: ignore[attr-defined]

    def in_warranty_period(self) -> bool:  # noqa: D102
        today = datetime.datetime.now(tz=datetime.UTC)
        return self.datetime_end_of_warranty > today

    in_warranty_period.admin_order_field = "datetime_end_of_warranty"  # type: ignore[attr-defined]
    in_warranty_period.boolean = True  # type: ignore[attr-defined]
    in_warranty_period.short_description = "Still Warranty ?"  # type: ignore[attr-defined]


MAX_QUANTITY = 999999


# validation in django administration
def validate_quantity(value: int) -> None:  # noqa: D103
    if value == 0 or value > MAX_QUANTITY:
        validation_err = f"{value} - is incorrect number, 1-999999 only!"
        raise ValidationError(validation_err)


class OrderItemAbstr(models.Model):
    """
    Abstract class for defining items to be ordered.

    This is required to store quantities, prices, etc. per item
    """

    position = models.IntegerField(null=True, blank=True, help_text="position/order of this item in basket / order")
    quantity = models.PositiveIntegerField(validators=[validate_quantity], default=1)

    item_price_net = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        default=0.0,
        help_text="net price for this item",
    )
    item_price_gross = models.DecimalField(
        blank=True,
        null=True,
        max_digits=10,
        decimal_places=2,
        default=0.0,
        help_text="gross price for this item",
    )
    item_transport_price = models.DecimalField(
        blank=True,
        null=True,
        max_digits=10,
        decimal_places=2,
        default=0.0,
        help_text="transport / shipping price for this item",
    )
    item_price_tax = models.DecimalField(
        blank=True,
        null=True,
        max_digits=10,
        decimal_places=2,
        default=0.0,
        help_text="tax rate for this item",
    )
    item_toll = models.DecimalField(
        blank=True,
        null=True,
        max_digits=10,
        decimal_places=2,
        default=0.0,
        help_text="toll for this item",
    )
    toll_number = models.TextField(blank=True, help_text="description of data type")
    item_discount = models.DecimalField(
        blank=True,
        null=True,
        max_digits=10,
        decimal_places=2,
        default=0.0,
        help_text="discount for this item",
    )
    charges = models.JSONField(
        blank=True,
        null=True,
        help_text="charges for the item, like toll, but also discounts ...",
    )
    total_price_net = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        default=0.0,
        help_text="total price for this item",
    )
    provider = models.ForeignKey(
        Entity,
        related_name="%(app_label)s_%(class)s_order_provider_related",
        related_query_name="%(app_label)s_%(class)s_order_provider_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="provider / company the item was ordered at",
    )
    status = models.ForeignKey(
        ItemStatus,
        related_name="%(app_label)s_%(class)s_orderitem_status_related",
        related_query_name="%(app_label)s_%(class)s_orderitem_status_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="status of the order item - can be used to mark the item as ordered, "
        "requested, quote, arrived delivered, invoiced, paid, ...",
    )

    class Meta:  # noqa: D106
        abstract = True


class ItemOrderAbstr(models.Model):
    """Abstract class for Ordering Items (OrderItems)."""

    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 order ",
    )
    name_display = models.TextField(
        blank=True,
        help_text="short, human readable title of order, can contain whitespaces",
    )
    name = models.TextField(
        blank=True,
        help_text="short name of order without whitespaces and special characters",
    )
    order_id_internal = models.TextField(blank=True, help_text="internal id / number of order")
    order_id_external = models.TextField(blank=True, help_text="external id / number of order")
    order_barcode = models.TextField(
        blank=True,
        unique=True,
        help_text="barcode of order, can be, e.g., used for tracking or in-house delivery.",
    )
    order_barcode_json = models.JSONField(
        blank=True,
        null=True,
        help_text="Advanced barcodes in JSON for multiple barcode representations, like QR codes etc.",
    )
    iri = models.URLField(
        blank=True,
        null=True,
        unique=True,
        max_length=512,
        help_text="International Resource Identifier - IRI: is used for semantic representation ",
    )
    datetime_order = models.DateTimeField(blank=True, null=True, help_text="date of order")

    entity_ordered = models.ForeignKey(
        Entity,
        related_name="%(app_label)s_%(class)s_entities_ordered_related",
        related_query_name="%(app_label)s_%(class)s_entities_ordered_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="entity who ordered the item",
    )
    group_ordered = models.ForeignKey(
        Group,
        related_name="%(app_label)s_%(class)s_groups_ordered_related",
        related_query_name="%(app_label)s_%(class)s_groups_ordered_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="group who ordered the item",
    )
    entity_ordering = models.ForeignKey(
        Entity,
        related_name="%(app_label)s_%(class)s_entities_ordering_related",
        related_query_name="%(app_label)s_%(class)s_entities_ordering_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="entity who is ordering the item (from an external resource like a company)",
    )
    group_ordering = models.ForeignKey(
        Group,
        related_name="%(app_label)s_%(class)s_groups_ordering_related",
        related_query_name="%(app_label)s_%(class)s_groups_ordering_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="group who is ordering the item (from an external resource like a company)",
    )
    provider = models.ForeignKey(
        Entity,
        related_name="%(app_label)s_%(class)s_order_provider_related",
        related_query_name="%(app_label)s_%(class)s_order_provider_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="provider / company providing the order.",
    )
    order_quote_id = models.TextField(blank=True, help_text="quote id of order")
    datetime_order_quote = models.DateTimeField(blank=True, null=True, help_text="date of latest quote")
    order_invoice_id = models.TextField(blank=True, help_text="invoice id of order")
    datetime_order_invoice = models.DateTimeField(blank=True, null=True, help_text="date of latest invoice")
    # transport / shipping
    transport_company = models.ForeignKey(
        Entity,
        related_name="%(app_label)s_%(class)s_transport_companies_related",
        related_query_name="%(app_label)s_%(class)s_transport_companies_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="company transport the item",
    )
    transport_tracking_id = models.TextField(blank=True, help_text="tracking ID of transport.")
    transport_price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        blank=True,
        null=True,
        help_text="price of transport / shipping",
    )
    # add budget
    budget_url = models.URLField(blank=True, max_length=512, help_text="Link to budget")
    datetime_order_pay = models.DateTimeField(
        default=timezone.now, blank=True, null=True, help_text="date of order payment"
    )

    @property
    def order_total_price_net(self) -> float:
        """Calculate the total price of the order net."""
        return sum([item.total_price_net for item in self.deviceorderitem_set.all()])

    order_total_price_gross = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        blank=True,
        null=True,
        help_text="total price of order gross",
    )

    order_total_price_tax = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        blank=True,
        null=True,
        help_text="total price of order TAX/VAT",
    )

    order_total_price_toll = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        blank=True,
        null=True,
        help_text="total price of order toll",
    )
    order_currency = models.ForeignKey(
        Currency,
        related_name="%(app_label)s_%(class)s_order_total_price_currencies_related",
        related_query_name="%(app_label)s_%(class)s_order_total_price_currencies_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="currency of order total price",
    )
    order_status = models.ForeignKey(
        ItemStatus,
        related_name="%(app_label)s_%(class)s_order_status_related",
        related_query_name="%(app_label)s_%(class)s_order_status_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="state of the order, like ordered, requested, quote, arrived delivered, invoiced, paid, ...",
    )
    charges = models.JSONField(
        blank=True,
        null=True,
        help_text="charges for the order, like toll, but also discounts ...",
    )
    datetime_last_modified = models.DateTimeField(
        default=timezone.now,
        blank=True,
        null=True,
        help_text="datetime of last changes of the order",
    )

    order_quotes = models.ManyToManyField(
        "ExtraData",  # String reference - will be resolved per app
        related_name="%(app_label)s_%(class)s_order_quotes",
        blank=True,
        help_text="Information about quotes for this order, like PDFs, Images, URLs, ...",
    )
    order_invoices = models.ManyToManyField(
        "ExtraData",  # String reference - will be resolved per app
        related_name="%(app_label)s_%(class)s_order_invoices",
        blank=True,
        help_text="Information about invoices for this order, like PDFs, Images, URLs, ...",
    )
    data_extra = models.ManyToManyField(
        "ExtraData",  # String reference - will be resolved per app
        related_name="%(app_label)s_%(class)s_data_extra",
        blank=True,
        help_text="additional data for this order",
    )

    search_vector = SearchVectorField(null=True)

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

    def __str__(self) -> str:  # noqa: D105
        return self.name or ""

    def __repr__(self) -> str:  # noqa: D105
        return self.name or ""

    def save(self, *args: Any, **kwargs: Any) -> None:  # noqa: D102
        if self.order_barcode == "":
            self.order_barcode = f"{self.name}_{self.pk}"
        super().save(*args, **kwargs)


class ItemBasketAbstr(models.Model):
    """
    Abstract class for collecting OrderItems in a basket or wishlist for (re-)ordering.

    :note: an item stash/local store can be generated by subclassing ItemBasketAbstr class and
           adding a location
    """

    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 the basket",
    )
    name_display = models.TextField(blank=True, help_text="Human readable name of basket, can contain spaces")
    name = models.TextField(
        unique=True,
        blank=True,
        help_text="Name of the basket without spaces and special characters, for accessing the basket",
    )
    entity = models.ForeignKey(
        Entity,
        related_name="%(app_label)s_%(class)s_entities_related",
        related_query_name="%(app_label)s_%(class)s_entities_related_query",
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        help_text="entity owning the basket",
    )
    datetime_created = models.DateTimeField(
        default=timezone.now,
        blank=True,
        null=True,
        help_text="datetime when the basket was created",
    )
    datetime_last_modified = models.DateTimeField(
        default=timezone.now,
        blank=True,
        null=True,
        help_text="datetime of last changes of the basket",
    )
    search_vector = SearchVectorField(null=True)

    class Meta:  # noqa: D106
        abstract = True
        indexes = (GinIndex(fields=["search_vector"]),)

    def add_item_instance(self, item_instance: Any, quantity: int = 1) -> None:
        """Standardized method name for adding items to baskets."""
        raise NotImplementedError("Subclasses must implement add_item_instance")
