from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.db import models
from django.db.models import (
    CheckConstraint,
    Index,
    Q,
    UUIDField,
)
from django.utils.functional import cached_property
from console_base.models import (  # type: ignore[attr-defined]
    BaseUUIDPKModel,
    CanonicalIdField,
    LCUniqueConstraint,
)
from ..querysets import (
    CanonicalIdDeleteLogManager,
    CanonicalIdMapManager,
    CanonicalIdSwapManager,
    SyncQuerySet,
    SYNC_TABLES_Q,
)


# ---------------------------------------------------------------------------
class GenericCanonicalIdTrackerMixin(BaseUUIDPKModel):
    """
    Mixin class to track Canonical ID record changes with
    Generic FK reference to database record.
    """

    cid = CanonicalIdField()
    name = models.TextField()
    table = models.ForeignKey(
        ContentType,
        on_delete=models.CASCADE,
        limit_choices_to=SYNC_TABLES_Q,
    )
    object_id = models.TextField(null=True, blank=True)
    record_object = GenericForeignKey(ct_field='table')

    class Meta:
        abstract = True
        indexes = [
            Index(fields=['object_id'], name='generic_record_pk_idx'),
            Index(fields=['cid'], name='cid_tracker_cid_idx'),
        ]

    @cached_property
    def app_label(self) -> str:
        return str(self.table.app_label)

    @cached_property
    def table_name(self) -> str:
        return str(self.table.model)


# ---------------------------------------------------------------------------
class CanonicalIdSwap(GenericCanonicalIdTrackerMixin):
    """
    When a Canonical ID needs to be changed on a record, keep
    a log of the prior value so that we can be sure to enforce
    that sync operations don't restore the old value.
    """

    css_icon = ''
    md_description = 'Swap Old for New Canonical ID'

    old = UUIDField()

    objects = CanonicalIdSwapManager()

    class Meta:
        app_label = 'concordia'
        verbose_name_plural = 'canonical id map'

        constraints = [
            CheckConstraint(
                name='old_cid_differs_from_new_cid',
                condition=Q(cid=models.F('old'), _negated=True),
                violation_error_message='Old CID must differ from new CID',
            ),
            LCUniqueConstraint(
                fields=('object_id', 'old'),
                name='old_canonical_id_unique',
                violation_error_message='Record swapping this old Canonical ID already exists.',
            ),
        ]

    def apply(self):
        """
        Apply this Canonical ID swap to the local database record.
        """
        try:
            record: BaseUUIDPKModel | None = self.record_object
            if record and record.cid != self.cid:
                record.cid = self.cid
                record.save(update_fields=['cid'])
        except models.Model.DoesNotExist:  # type: ignore[attr-defined]
            pass


# ---------------------------------------------------------------------------
class CanonicalIdMap(GenericCanonicalIdTrackerMixin):
    """
    Records have a Canonical ID field that should identify the record
    throughout all portal consoles. However, if it's not possible to
    have a record's CID be globally unique, this model will store the
    relationship of the local CID to the global CID.

    Examples might be records synced with an Active Directory server,
    where the AD server's UUID value is used as CID. It's not possible
    to change the AD server's value, so a mapping entry can be made.
    """

    css_icon = ''
    md_description = 'Map Local Canonical ID to the Global Canonical ID'

    local = UUIDField()

    objects = CanonicalIdMapManager()

    class Meta:
        app_label = 'concordia'
        verbose_name_plural = 'canonical id map'

        constraints = [
            CheckConstraint(
                name='local_cid_differs_from_global_cid',
                condition=Q(cid=models.F('local'), _negated=True),
                violation_error_message='Local CID must differ from global CID',
            ),
            LCUniqueConstraint(
                fields=('object_id', 'local'),
                name='local_canonical_id_unique',
                condition=Q(object_id__isnull=False),
                violation_error_message='Local record mapping to Canonical ID already exists.',
            ),
        ]

    def apply(self):
        """
        Apply this Canonical ID Map to the local database record.
        """
        try:
            record: BaseUUIDPKModel | None = self.record_object
            if record and record.cid != self.local:
                self.local = record.cid
                record.save(update_fields=['local'])
        except models.Model.DoesNotExist:  # type: ignore[attr-defined]
            pass


# ---------------------------------------------------------------------------
class CanonicalIdDeleteLog(GenericCanonicalIdTrackerMixin):
    """
    Canonical IDs that have been deleted cannot be re-used.

    These records will normally be created on Sync Publishers
    to ensure that Sync Subscribers or other clients cannot
    re-create a deleted record automatically. If a record
    is to be recreated, a new Canonical ID can be used,
    or the CanonicalIdDeleteLog record be deleted.
    """

    css_icon = ''
    md_description = 'Canonical ID Deletion Record'

    objects = CanonicalIdDeleteLogManager()

    class Meta:
        app_label = 'concordia'
        verbose_name_plural = 'canonical id delete'

        constraints = [
            LCUniqueConstraint(
                fields=['cid'],
                name='deleted_canonical_id_unique',
                violation_error_message='Delete record for this Canonical ID already exists.',
            ),
        ]

    def apply(self):
        """
        Ensure that this record is deleted.
        """
        if not self.record_object:
            return

        self.record_object.delete()


# ---------------------------------------------------------------------------
class SyncableModel(BaseUUIDPKModel):
    """
    Mixin class to add sync methods for type hints
    """

    LOCAL_CONSTRAINT_FIELDS: frozenset = frozenset()
    cid = CanonicalIdField()

    objects = SyncQuerySet.as_manager()

    class Meta:
        abstract = True

    def is_syncable(self) -> bool:
        return True


__all__ = (
    'GenericCanonicalIdTrackerMixin',
    'CanonicalIdSwap',
    'CanonicalIdMap',
    'CanonicalIdDeleteLog',
    'SyncableModel',
)
