from __future__ import unicode_literals

import copy
import datetime
from django.db import models
from django.utils.functional import curry
from django.utils.translation import ugettext_lazy as _
from django.conf import settings

from audit_log.models.fields import LastUserField


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 getattr(settings, 'DISABLE_AUDIT_LOG', False):
            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.rel and field.rel.related_name:
                    field.rel.related_name = '_auditlog_%s' % field.rel.related_name






                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.
        """

        def convert(name):
            import re
            s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
            return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()

        db_table = ''
        if model._meta.app_label:
            db_table = model._meta.app_label + '_'
        db_table += '%s_history' % convert(model._meta.object_name)

        result = {
            'ordering' : ('-action_date',),
            'app_label' : model._meta.app_label,
            'db_table' : db_table,
        }
        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('%sHistory'%model._meta.object_name)
        return type(name, (models.Model,), attrs)
