from datetime import date

from django.contrib.postgres.constraints import ExclusionConstraint
from django.contrib.postgres.fields import DateRangeField, RangeOperators
from django.db import models, transaction
from django.utils.module_loading import import_string
from psycopg.types.range import DateRange
from slugify import slugify
from wbcore.contrib.io.mixins import ImportMixin
from wbcore.contrib.notifications.utils import create_notification_type
from wbcore.models import WBModel
from wbcore.utils.models import ComplexToStringMixin

from wbfdm.import_export.handlers.instrument_list import InstrumentListImportHandler
from wbfdm.models.instruments.instruments import Instrument


class InstrumentListThroughModel(ImportMixin, ComplexToStringMixin):
    import_export_handler_class = InstrumentListImportHandler
    """
    This model is not a Through model from a programming point of view, however it allows to link instrument list to
    instruments.
    """

    instrument_str = models.CharField(max_length=256)
    instrument = models.ForeignKey(
        to="wbfdm.Instrument",
        null=True,
        blank=True,
        limit_choices_to=models.Q(is_security=True),
        on_delete=models.SET_NULL,
    )
    instrument_list = models.ForeignKey(
        to="wbfdm.InstrumentList",
        on_delete=models.CASCADE,
    )
    timespan = DateRangeField(verbose_name="Timespan")
    comment = models.TextField(default="", blank=True)
    validated = models.BooleanField(default=False)

    def save(self, *args, **kwargs):
        if not self.timespan:
            self.timespan = DateRange(date.today(), None)
        super().save(*args, **kwargs)

    def compute_str(self) -> str:
        """
        Method to compute the string representation of the instance. It will save the string value to the computed_str
        field.

        Returns:
            The string representation of the instance.
        """
        if self.instrument and self.instrument.name_repr:
            return f"{self.instrument.name_repr} - {self.instrument_list.name}"
        return f"{self.instrument_str} - {self.instrument_list.name}"

    class Meta:
        verbose_name = "Instrument in Instrument List"
        constraints = [
            ExclusionConstraint(
                name="exclude_overlapping_instrument_list",
                condition=models.Q(instrument__isnull=False),
                expressions=[
                    ("timespan", RangeOperators.OVERLAPS),
                    ("instrument", RangeOperators.EQUAL),
                    ("instrument_list", RangeOperators.EQUAL),
                ],
            ),
        ]
        notification_types = [
            create_notification_type(
                "wbfdm.instrument_list_add",
                "Instrument added to Instrument List",
                "A notification when an instrument gets added to a list.",
                True,
                True,
                True,
            ),
        ]


class InstrumentList(WBModel):
    class InstrumentListType(models.TextChoices):
        WATCH = "WATCH", "Watch List"
        EXCLUSION = "EXCLUSION", "Exclusion List"
        INCLUSION = "INCLUSION", "Inclusion List"

    name = models.CharField(max_length=255)
    identifier = models.CharField(max_length=255, unique=True, blank=True)
    instrument_list_type = models.CharField(max_length=32, choices=InstrumentListType.choices, null=True, blank=True)
    constituent_loader_path = models.CharField(
        max_length=512,
        null=True,
        blank=True,
        help_text="If set, load the Instrument queryset returned by this function and set them as the new instrument list constituent",
    )
    constituent_loader_kwargs = models.JSONField(default=dict, blank=True)

    def __str__(self):
        return self.name

    def get_instruments(self, val_date: date, only_validated: bool = False) -> models.QuerySet[Instrument]:
        """
        Returns a QuerySet of Instrument objects associated with the current instrument list.

        This property filters the Instrument objects based on the related InstrumentListThroughModel
        and returns only those Instruments where the foreign key is not null.

        Returns:
            models.QuerySet[Instrument]: A QuerySet of Instrument objects.
        """
        rels = InstrumentListThroughModel.objects.filter(
            models.Q(instrument_list=self) & models.Q(instrument__isnull=False) & models.Q(timespan__contains=val_date)
        )
        if only_validated:
            rels = rels.filter(validated=True)
        return Instrument.objects.filter(id__in=(rels.values("instrument")))

    def save(self, *args, **kwargs):
        if not self.identifier:
            self.identifier = slugify(f"{self.name}-{self.id}")
        super().save(*args, **kwargs)

    def load_constituent(self, val_date: date) -> list[Instrument]:
        if self.constituent_loader_path:
            return import_string(self.constituent_loader_path)(self, val_date, **self.constituent_loader_kwargs)
        return []

    def refresh_constituent_from_list(self, val_date: date, instruments: list[Instrument]):
        active_instruments = self.get_instruments(val_date)
        active_instrument_ids = set(active_instruments.values_list("id", flat=True))
        new_instrument_ids = {instr.id for instr in instruments}
        objs_to_close = []

        with transaction.atomic():
            for rel_to_close in InstrumentListThroughModel.objects.filter(
                instrument_list=self, instrument_id__in=active_instrument_ids - new_instrument_ids
            ):
                rel_to_close.timespan = DateRange(rel_to_close.timespan.lower, val_date)
                objs_to_close.append(rel_to_close)
            if objs_to_close:
                InstrumentListThroughModel.objects.bulk_update(objs_to_close, ["timespan"], batch_size=10000)

            objs_to_create = []
            for instr in instruments:
                if instr.id not in active_instrument_ids:
                    objs_to_create.append(
                        InstrumentListThroughModel(
                            timespan=DateRange(val_date, None), instrument=instr, instrument_list=self
                        )
                    )
            if objs_to_create:
                InstrumentListThroughModel.objects.bulk_create(objs_to_create, batch_size=10000)

    class Meta:
        verbose_name = "Instrument List"
        verbose_name_plural = "Instrument Lists"
        permissions = (("administrate_instrumentlist", "Can administrate Instrument List"),)

    @classmethod
    def get_endpoint_basename(cls):
        return "wbfdm:instrumentlist"

    @classmethod
    def get_representation_endpoint(cls):
        return "wbfdm:instrumentlistrepresentation-list"

    @classmethod
    def get_representation_value_key(cls):
        return "id"

    @classmethod
    def get_representation_label_key(cls):
        return "{{name}}"
