import copy
import datetime
from functools import partial
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.conf import settings

from logs.models.fields import LastUserField
from logs import settings as local_settings


try:
    from django.utils.timezone import now as datetime_now

    assert datetime_now
except ImportError:
    import datetime

    datetime_now = datetime.datetime.now


class LogEntryObjectDescriptor(object):
    def __init__(self, model):
        self.model = model

    def __get__(self, instance, owner):
        kwargs = dict(
            (f.attname, getattr(instance, f.attname))
            for f in self.model._meta.fields
            if hasattr(instance, f.attname)
        )
        return self.model(**kwargs)


class AuditLogManager(models.Manager):
    def __init__(
        self,
        model,
        attname,
        instance=None,
    ):
        super(AuditLogManager, self).__init__()
        self.model = model
        self.instance = instance
        self.attname = attname
        # set a hidden attribute on the  instance to control wether we should track changes
        if instance is not None and not hasattr(instance, "__is_%s_enabled" % attname):
            setattr(instance, "__is_%s_enabled" % attname, True)

    def enable_tracking(self):
        if self.instance is None:
            raise ValueError(
                "Tracking can only be enabled or disabled "
                "per model instance, not on a model class"
            )
        setattr(self.instance, "__is_%s_enabled" % self.attname, True)

    def disable_tracking(self):
        if self.instance is None:
            raise ValueError(
                "Tracking can only be enabled or disabled "
                "per model instance, not on a model class"
            )
        setattr(self.instance, "__is_%s_enabled" % self.attname, False)

    def is_tracking_enabled(self):
        if local_settings.DISABLE_AUDIT_LOG:
            return False
        if self.instance is None:
            raise ValueError(
                "Tracking can only be enabled or disabled "
                "per model instance, not on a model class"
            )
        return getattr(self.instance, "__is_%s_enabled" % self.attname)

    def get_queryset(self):
        if self.instance is None:
            return super(AuditLogManager, self).get_queryset()

        f = {self.instance._meta.pk.name: self.instance.pk}
        return super(AuditLogManager, self).get_queryset().filter(**f)


class AuditLogDescriptor(object):
    def __init__(self, model, manager_class, attname):
        self.model = model
        self.manager_class = manager_class
        self.attname = attname

    def __get__(self, instance, owner):
        if instance is None:
            return self.manager_class(self.model, self.attname)
        return self.manager_class(self.model, self.attname, instance)


class AuditLog(object):
    manager_class = AuditLogManager

    def __init__(self, exclude=[]):
        self._exclude = exclude

    def contribute_to_class(self, cls, name):
        self.manager_name = name
        models.signals.class_prepared.connect(self.finalize, sender=cls)

    def create_log_entry(self, instance, action_type):
        manager = getattr(instance, self.manager_name)
        attrs = {}
        for field in instance._meta.fields:
            if field.attname not in self._exclude:
                attrs[field.attname] = getattr(instance, field.attname)
        manager.create(action_type=action_type, **attrs)

    def post_save(self, instance, created, **kwargs):
        # ignore if it is disabled
        if getattr(instance, self.manager_name).is_tracking_enabled():
            self.create_log_entry(instance, created and "I" or "U")

    def post_delete(self, instance, **kwargs):
        # ignore if it is disabled
        if getattr(instance, self.manager_name).is_tracking_enabled():
            self.create_log_entry(instance, "D")

    def finalize(self, sender, **kwargs):
        log_entry_model = self.create_log_entry_model(sender)

        models.signals.post_save.connect(self.post_save, sender=sender, weak=False)
        models.signals.post_delete.connect(self.post_delete, sender=sender, weak=False)

        descriptor = AuditLogDescriptor(
            log_entry_model, self.manager_class, self.manager_name
        )
        setattr(sender, self.manager_name, descriptor)

    def copy_fields(self, model):
        """
        Creates copies of the fields we are keeping
        track of for the provided model, returning a
        dictionary mapping field name to a copied field object.
        """
        fields = {"__module__": model.__module__}

        for field in model._meta.fields:
            if not field.name in self._exclude:
                field = copy.deepcopy(field)

                if isinstance(field, models.AutoField):
                    # we replace the AutoField of the original model
                    # with an IntegerField because a model can
                    # have only one autofield.

                    field.__class__ = models.IntegerField

                if field.primary_key:
                    field.serialize = True

                # OneToOne fields should really be tracked
                # as ForeignKey fields
                if isinstance(field, models.OneToOneField):
                    field.__class__ = models.ForeignKey

                if field.primary_key or field.unique:
                    # unique fields of the original model
                    # can not be guaranteed to be unique
                    # in the audit log entry but they
                    # should still be indexed for faster lookups.

                    field.primary_key = False
                    field._unique = False
                    field.db_index = True

                if field.remote_field and field.remote_field.related_name:
                    field.remote_field.related_name = "_auditlog_{}_{}".format(
                        model._meta.model_name, field.remote_field.related_name
                    )
                elif field.remote_field:
                    try:
                        if field.remote_field.get_accessor_name():
                            field.remote_field.related_name = "_auditlog_{}_{}".format(
                                model._meta.model_name,
                                field.remote_field.get_accessor_name(),
                            )
                    except e:
                        pass

                fields[field.name] = field

        return fields

    def get_logging_fields(self, model):
        """
        Returns a dictionary mapping of the fields that are used for
        keeping the acutal audit log entries.
        """
        rel_name = "_%s_audit_log_entry" % model._meta.object_name.lower()

        def entry_instance_to_unicode(log_entry):
            try:
                result = "%s: %s %s at %s" % (
                    model._meta.object_name,
                    log_entry.object_state,
                    log_entry.get_action_type_display().lower(),
                    log_entry.action_date,
                )
            except AttributeError:
                result = "%s %s at %s" % (
                    model._meta.object_name,
                    log_entry.get_action_type_display().lower(),
                    log_entry.action_date,
                )
            return result

        action_user_field = LastUserField(related_name=rel_name, editable=False)

        # check if the manager has been attached to auth user model
        if [model._meta.app_label, model.__name__] == getattr(
            settings, "AUTH_USER_MODEL", "auth.User"
        ).split("."):
            action_user_field = LastUserField(
                related_name=rel_name, editable=False, to="self"
            )

        return {
            "action_id": models.AutoField(primary_key=True),
            "action_date": models.DateTimeField(
                default=datetime_now, editable=False, blank=False
            ),
            "action_user": action_user_field,
            "action_type": models.CharField(
                max_length=1,
                editable=False,
                choices=(
                    ("I", _("Created")),
                    ("U", _("Changed")),
                    ("D", _("Deleted")),
                ),
            ),
            "object_state": LogEntryObjectDescriptor(model),
            "__unicode__": entry_instance_to_unicode,
        }

    def get_meta_options(self, model):
        """
        Returns a dictionary of Meta options for the
        autdit log model.
        """
        result = {
            "ordering": ("-action_date",),
            "app_label": model._meta.app_label,
        }
        from django.db.models.options import DEFAULT_NAMES

        if "default_permissions" in DEFAULT_NAMES:
            result.update({"default_permissions": ()})
        return result

    def create_log_entry_model(self, model):
        """
        Creates a log entry model that will be associated with
        the model provided.
        """

        attrs = self.copy_fields(model)
        attrs.update(self.get_logging_fields(model))
        attrs.update(Meta=type(str("Meta"), (), self.get_meta_options(model)))
        name = str("%sAuditLogEntry" % model._meta.object_name)
        return type(name, (models.Model,), attrs)
