from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

from core.models import ObjectType
from extras.choices import *
from ..querysets import ObjectChangeQuerySet

__all__ = (
    'ObjectChange',
)


class ObjectChange(models.Model):
    """
    Record a change to an object and the user account associated with that change. A change record may optionally
    indicate an object related to the one being changed. For example, a change to an interface may also indicate the
    parent device. This will ensure changes made to component models appear in the parent model's changelog.
    """
    time = models.DateTimeField(
        verbose_name=_('time'),
        auto_now_add=True,
        editable=False,
        db_index=True
    )
    user = models.ForeignKey(
        to=settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        related_name='changes',
        blank=True,
        null=True
    )
    user_name = models.CharField(
        verbose_name=_('user name'),
        max_length=150,
        editable=False
    )
    request_id = models.UUIDField(
        verbose_name=_('request ID'),
        editable=False,
        db_index=True
    )
    action = models.CharField(
        verbose_name=_('action'),
        max_length=50,
        choices=ObjectChangeActionChoices
    )
    changed_object_type = models.ForeignKey(
        to='contenttypes.ContentType',
        on_delete=models.PROTECT,
        related_name='+'
    )
    changed_object_id = models.PositiveBigIntegerField()
    changed_object = GenericForeignKey(
        ct_field='changed_object_type',
        fk_field='changed_object_id'
    )
    related_object_type = models.ForeignKey(
        to='contenttypes.ContentType',
        on_delete=models.PROTECT,
        related_name='+',
        blank=True,
        null=True
    )
    related_object_id = models.PositiveBigIntegerField(
        blank=True,
        null=True
    )
    related_object = GenericForeignKey(
        ct_field='related_object_type',
        fk_field='related_object_id'
    )
    object_repr = models.CharField(
        max_length=200,
        editable=False
    )
    prechange_data = models.JSONField(
        verbose_name=_('pre-change data'),
        editable=False,
        blank=True,
        null=True
    )
    postchange_data = models.JSONField(
        verbose_name=_('post-change data'),
        editable=False,
        blank=True,
        null=True
    )

    objects = ObjectChangeQuerySet.as_manager()

    class Meta:
        ordering = ['-time']
        indexes = (
            models.Index(fields=('changed_object_type', 'changed_object_id')),
            models.Index(fields=('related_object_type', 'related_object_id')),
        )
        verbose_name = _('object change')
        verbose_name_plural = _('object changes')

    def __str__(self):
        return '{} {} {} by {}'.format(
            self.changed_object_type,
            self.object_repr,
            self.get_action_display().lower(),
            self.user_name
        )

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

        # Validate the assigned object type
        if self.changed_object_type not in ObjectType.objects.with_feature('change_logging'):
            raise ValidationError(
                _("Change logging is not supported for this object type ({type}).").format(
                    type=self.changed_object_type
                )
            )

    def save(self, *args, **kwargs):

        # Record the user's name and the object's representation as static strings
        if not self.user_name:
            self.user_name = self.user.username
        if not self.object_repr:
            self.object_repr = str(self.changed_object)

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

    def get_absolute_url(self):
        return reverse('extras:objectchange', args=[self.pk])

    def get_action_color(self):
        return ObjectChangeActionChoices.colors.get(self.action)

    @property
    def has_changes(self):
        return self.prechange_data != self.postchange_data
