from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey

from netbox.models.features import *
from netbox.models.ltree import LtreeManager, LtreeModel, SortPathField
from netbox.models.mixins import OwnerMixin
from utilities.mptt import TreeManager
from utilities.querysets import RestrictedQuerySet

__all__ = (
    'AdminModel',
    'ChangeLoggedModel',
    'NestedGroupModel',
    'NestedGroupModelMixin',
    'NestedLtreeGroupModel',
    'NetBoxModel',
    'OrganizationalModel',
    'PrimaryModel',
)


class NetBoxFeatureSet(
    BookmarksMixin,
    ChangeLoggingMixin,
    CloningMixin,
    CustomFieldsMixin,
    CustomLinksMixin,
    CustomValidationMixin,
    ExportTemplatesMixin,
    JournalingMixin,
    NotificationsMixin,
    TagsMixin,
    EventRulesMixin
):
    class Meta:
        abstract = True

    @property
    def docs_url(self):
        return f'{settings.STATIC_URL}docs/models/{self._meta.app_label}/{self._meta.model_name}/'

    def get_absolute_url(self):
        from utilities.views import get_viewname
        return reverse(get_viewname(self), args=[self.pk])


#
# Base model classes
#

class BaseModel(models.Model):
    """
    A global base model for all NetBox objects.

    This class provides some important overrides to Django's default functionality, such as
    - Overriding the default manager to use RestrictedQuerySet
    - Extending `clean()` to validate GenericForeignKey fields
    - Extending `clean()` and `save()` to coerce empty strings to None on unique nullable CharFields
    """

    objects = RestrictedQuerySet.as_manager()

    class Meta:
        abstract = True

    def _coerce_nullable_unique_chars(self):
        """
        Coerce empty strings to None on unique nullable CharFields to avoid spurious
        uniqueness violations (PostgreSQL treats two empty strings as duplicates).
        """
        for field in self._meta.concrete_fields:
            if (
                isinstance(field, models.CharField)
                and field.null
                and field.unique
                and getattr(self, field.attname, None) == ''
            ):
                setattr(self, field.attname, None)

    def clean(self):
        """
        Validate the model for GenericForeignKey fields to ensure that the content type and object ID exist.
        """
        super().clean()
        self._coerce_nullable_unique_chars()

        for field in self._meta.get_fields():
            if isinstance(field, GenericForeignKey):
                ct_value = getattr(self, field.ct_field, None)
                fk_value = getattr(self, field.fk_field, None)

                if ct_value is None and fk_value is not None:
                    raise ValidationError({
                        field.ct_field: "This field cannot be null.",
                    })
                if fk_value is None and ct_value is not None:
                    raise ValidationError({
                        field.fk_field: "This field cannot be null.",
                    })

                if ct_value and fk_value:
                    klass = getattr(self, field.ct_field).model_class()
                    try:
                        obj = klass.objects.get(pk=fk_value)
                    except ObjectDoesNotExist:
                        raise ValidationError({
                            field.fk_field: f"Related object not found using the provided value: {fk_value}."
                        })

                    # update the GFK field value
                    setattr(self, field.name, obj)

    def save(self, *args, **kwargs):
        self._coerce_nullable_unique_chars()
        super().save(*args, **kwargs)


class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, EventRulesMixin, BaseModel):
    """
    Base model for ancillary models; provides limited functionality for models which don't
    support NetBox's full feature set.
    """

    class Meta:
        abstract = True


class NetBoxModel(NetBoxFeatureSet, BaseModel):
    """
    Base model for most object types. Suitable for use by plugins.
    """

    class Meta:
        abstract = True


#
# NetBox internal base models
#

class PrimaryModel(OwnerMixin, NetBoxModel):
    """
    Primary models represent real objects within the infrastructure being modeled.
    """
    description = models.CharField(
        verbose_name=_('description'),
        max_length=200,
        blank=True
    )
    comments = models.TextField(
        verbose_name=_('comments'),
        blank=True
    )

    class Meta:
        abstract = True


class NestedGroupModelMixin(OwnerMixin, NetBoxModel):
    """
    Shared field set and behavior for hierarchical group models. Concrete bases supply the
    tree backend (MPTT or ltree) and the corresponding `parent` ForeignKey / manager.
    """
    name = models.CharField(
        verbose_name=_('name'),
        max_length=100
    )
    slug = models.SlugField(
        verbose_name=_('slug'),
        max_length=100
    )
    description = models.CharField(
        verbose_name=_('description'),
        max_length=200,
        blank=True
    )
    comments = models.TextField(
        verbose_name=_('comments'),
        blank=True
    )

    class Meta:
        abstract = True

    def __str__(self):
        return self.name


class NestedGroupModel(NestedGroupModelMixin, MPTTModel):
    """
    Deprecated MPTT-backed nested group base, retained for backwards compatibility with plugins.

    New code (in NetBox core and in plugins) should use `NestedLtreeGroupModel` instead. This
    class will be removed in a future release once the deprecation period has elapsed.
    """
    parent = TreeForeignKey(
        to='self',
        on_delete=models.CASCADE,
        related_name='children',
        blank=True,
        null=True,
        db_index=True
    )

    objects = TreeManager()

    class Meta:
        abstract = True

    class MPTTMeta:
        order_insertion_by = ('name',)

    def clean(self):
        super().clean()

        # A nested group cannot be its own parent or a descendant of itself. The ltree
        # base (NestedLtreeGroupModel) enforces this via LtreeModel.clean(); this MPTT
        # variant keeps the original get_descendants()-based check.
        if not self._state.adding and self.parent and self.parent in self.get_descendants(include_self=True):
            raise ValidationError({
                "parent": _("Cannot assign self or a descendant as parent.")
            })


class NestedLtreeGroupModel(NestedGroupModelMixin, LtreeModel):
    """
    Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
    recursively using PostgreSQL ltree. Within each parent, each child instance must have a unique name.

    `sort_path` is a trigger-maintained text column holding a chr(9)-separated chain of ancestor
    names; ordering by it yields tree-flatten output with siblings in name (collation) order.
    Inserts, reparents, AND renames all update `sort_path` (a rename cascades to descendants), so
    list ordering reflects renames immediately — unlike django-mptt's `order_insertion_by`, which
    left descendants stale until a manual rebuild.
    """
    parent = models.ForeignKey(
        to='self',
        on_delete=models.CASCADE,
        related_name='children',
        blank=True,
        null=True,
        db_index=True
    )
    sort_path = SortPathField(
        editable=False,
        blank=True,
        default='',
    )

    # Re-declare so the LtreeManager wins over BaseModel's RestrictedQuerySet
    # default manager via MRO resolution.
    objects = LtreeManager()

    class Meta:
        abstract = True
        ordering = ('sort_path',)


class OrganizationalModel(OwnerMixin, NetBoxModel):
    """
    Organizational models are those which are used solely to categorize and qualify other objects, and do not convey
    any real information about the infrastructure being modeled (for example, functional device roles). Organizational
    models provide the following standard attributes:
    - Unique name
    - Unique slug (automatically derived from name)
    - Optional description
    """
    name = models.CharField(
        verbose_name=_('name'),
        max_length=100,
        unique=True
    )
    slug = models.SlugField(
        verbose_name=_('slug'),
        max_length=100,
        unique=True
    )
    description = models.CharField(
        verbose_name=_('description'),
        max_length=200,
        blank=True
    )
    comments = models.TextField(
        verbose_name=_('comments'),
        blank=True
    )

    class Meta:
        abstract = True
        ordering = ('name',)

    def __str__(self):
        return self.name


class AdminModel(
    BookmarksMixin,
    CloningMixin,
    CustomLinksMixin,
    CustomValidationMixin,
    EventRulesMixin,
    ExportTemplatesMixin,
    NotificationsMixin,
    BaseModel,
):
    """
    A model which represents an administrative resource.
    """
    description = models.CharField(
        verbose_name=_('description'),
        max_length=200,
        blank=True
    )

    class Meta:
        abstract = True
